重构:更新API基础URL并增强内容解析逻辑
- 更改了API基础URL,以便在开发和生产环境之间切换。 - 添加了assetBase参数,以提高内容解析中图像源的分辨率。 - 增强了阅读页面的配置,将assetBase纳入图像URL的配置中。 - 更新了OpenPlatformPage,以便正确导航至设置和API文档。 - 对ContentPage进行了重构,以包含额外的webhook配置检索功能。 - 改进了DashboardPage,使其能更有效地处理用户交互。 - 清理了未使用的导入,并优化了AdminLayout,以提高可读性。
This commit is contained in:
88
.cursor/agent/智能体-技能-经验总索引.md
Normal file
88
.cursor/agent/智能体-技能-经验总索引.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# 智能体技能&经验总索引
|
||||
|
||||
用途:让任何智能体在“开始实现或回答”之前,先快速匹配“有没有相关经验”和“应该用哪个 Skill”,再综合处理。
|
||||
|
||||
---
|
||||
|
||||
## 1. 经验索引(按角色)
|
||||
|
||||
- 助理橙子:`agent/开发助理/evolution/索引.md`
|
||||
- 2026-03-21:MBTI 头像小程序全链路兜底 + 匹配接口回填
|
||||
- 2026-03-16:用户交互习惯分析(基于 agent-transcripts)
|
||||
- 小程序开发工程师:`agent/小程序开发工程师/evolution/索引.md`
|
||||
- 2026-03-30:阅读页 `navigateToMiniProgram` 时 path 追加 `phone`
|
||||
- 2026-03-17:代付美团式流程与目录 loading/折叠
|
||||
- 2026-03-16:编辑资料页分享名片(Canvas 封面)
|
||||
- 管理端开发工程师:`agent/管理端开发工程师/evolution/索引.md`
|
||||
- 2026-03-16:链接人与事(复制/删除 Dialog、planId/apiKey 列)
|
||||
- 2026-03-10:迁移 Mycontent-temp(主导航收敛/Settings 承载)
|
||||
- 2026-03-05:文章详情@某人(编辑页插入 @ 用户)
|
||||
- 后端工程师:`agent/后端工程师/evolution/索引.md`
|
||||
- 2026-03-24:router 缺失四 handler 补齐(BookRanking 等)
|
||||
- 2026-03-17:PayNotify 权益归属发起人
|
||||
- 2026-03-14:内容排名算法修正(rank 分公式)
|
||||
- 产品经理:`agent/产品经理/evolution/索引.md`
|
||||
- 2026-03-24:开发进度同步会议(文档同步原则/项目索引补齐)
|
||||
- 2026-03-10:需求同步(管理端迁移入口策略)
|
||||
- 2026-03-05:文章详情@某人(验收标准与待确认)
|
||||
- 软件测试:`agent/软件测试/evolution/索引.md`
|
||||
- 2026-03-16:scripts/test/process 流程测试目录
|
||||
- 2026-03-05:文章详情@某人(用例、联调与回归点)
|
||||
- 2026-03-10:管理端迁移回归(鉴权/跳转可达性)
|
||||
- 安全工程师:`agent/安全工程师/evolution/索引.md`
|
||||
- 2026-03-20:挖矿病毒排查经验转化为 Skills
|
||||
- 2026-03-20:部署 API / 部署触发词(环境与脚本映射)
|
||||
- 团队(跨角色共享):`agent/团队/evolution/索引.md`
|
||||
- 2026-03-16:TipTap Mention 需 data-label
|
||||
- 2026-03-17:代付美团式流程与权益归属约定
|
||||
- 2026-03-24:文档同步原则(实现变更后同步需求/运营/索引)
|
||||
- 老板分身:`agent/老板分身/evolution/索引.md`
|
||||
|
||||
---
|
||||
|
||||
## 2. 技能索引(按任务类型/触发)
|
||||
|
||||
- 小程序开发(WXML/WXSS/JS、`/api/miniprogram/*`):`skills/miniprogram-dev/SKILL.md`
|
||||
- 触发:`miniprogram/`、小程序、微信原生、C 端能力改动
|
||||
- 管理端开发(React/Vite、`/api/admin/*`、`/api/db/*`、禁止 `alert`):`skills/admin-dev/SKILL.md`
|
||||
- 触发:`soul-admin/`、管理端、React 后台、调用 admin/db 接口
|
||||
- 后端开发(Go/Gin/GORM、路由分组):`skills/api-dev/SKILL.md`
|
||||
- 触发:`soul-api`、API、Go、GORM、新增接口/路由分组
|
||||
- 产品经理(需求分析/验收/加个需求):`skills/product-manager/SKILL.md`
|
||||
- 触发:需求分析、需求文档、验收、加个需求
|
||||
- 测试(功能/回归/三端联调):`skills/testing/SKILL.md`
|
||||
- 触发:测试、测试用例、回归测试、QA
|
||||
- 助理橙子(文档同步/经验入库/会议结束/日志):`skills/assistant-doc-sync/SKILL.md`
|
||||
- 触发:小橙/橙子/橙橙/🍊、讨论完毕、吸收经验、保存进度、会议结束、出日志
|
||||
- 团队会议(多角色发言、决议、纪要):`skills/team-meeting/SKILL.md`
|
||||
- 触发:开个会、团队会议、需求评审、方案讨论
|
||||
- 变更关联检查(防漏改,改完必过):`skills/change-checklist/SKILL.md`
|
||||
- 触发:变更完成、检查一下、准备提交
|
||||
- 跨端协同流程(职责分工/联想补齐):`skills/role-flow-control/SKILL.md`
|
||||
- 触发:跨端功能开发、三端联动优化
|
||||
- 三端架构分析/迁移拆解约束:`skills/three-tier-arch/SKILL.md`
|
||||
- 触发:拆解、迁移、框架选择、三端架构
|
||||
- 新版快速分析(甲方/第三方改版补齐逻辑与接口规范):`skills/new-version-analyze/SKILL.md`
|
||||
- 触发:新版分析、版本对比、迁移分析、甲方代码分析
|
||||
- next-project 仅预览说明:`skills/next-preview/SKILL.md`
|
||||
- 触发:区分线上后端与 next-project 预览
|
||||
- Next.js 拆解为前后端分离+小程序:`skills/next-split/SKILL.md`
|
||||
- 触发:Next 全栈拆解、架构迁移
|
||||
- MySQL 直连操作(MCP 不通时执行 DDL/DML):`skills/mysql-direct/SKILL.md`
|
||||
- 触发:数据库操作、增减字段、紧急迁移、MCP 连接失败
|
||||
- 挖矿病毒排查与防护(xmrig/miner_guard 等):`skills/security-miner-guard/SKILL.md`
|
||||
- 触发:挖矿病毒、xmrig、服务器被入侵、杀挖矿
|
||||
- 服务器操作与部署(SSH/宝塔/Cunkebao):`skills/security-server-ops/SKILL.md`
|
||||
- 触发:部署、服务器操作、SSH、devloy、master
|
||||
- 卡若创业派对运营(报表/切片/分发/飞书视频文字下载):`skills/karuo-party/SKILL.md`
|
||||
- 触发:运营报表、视频切片、多平台分发、飞书视频下载、派对运营
|
||||
|
||||
---
|
||||
|
||||
## 3. 使用流程(任何智能体都照做)
|
||||
|
||||
1. 先归类:根据编辑目录或触发词判断“可能涉及哪些角色/任务类型”。
|
||||
2. 再查经验:打开对应角色的 `evolution/索引.md`,找最近且最贴近的经验条目。
|
||||
3. 再查技能:打开对应 Skill 的 `SKILL.md`,按其执行流程/约束落地。
|
||||
4. 再综合:若命中多个条目,按“边界规则 + 变更闭环(如 change-checklist)+ 必要的联动(如会议/跨端)”合并决策。
|
||||
|
||||
26
.cursor/plans/vip-members-person-文章绑定超级个体.md
Normal file
26
.cursor/plans/vip-members-person-文章绑定超级个体.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: vip-members Person 聚合 + 文章绑定超级个体
|
||||
overview: 见 Cursor 计划「vip-members_person_聚合」;含文章绑定超级个体与白名单 Webhook。执行前以本文件与 Cursor plan 同步为准。
|
||||
---
|
||||
|
||||
完整内容与 todos 见 IDE 内计划:`vip-members_person_聚合_2b9cac41.plan.md`(或从命令面板打开 Current Plan)。
|
||||
|
||||
要点摘要:
|
||||
|
||||
1. **原有三项**:vip-members 聚合 Person、UsersPage @列、ContentPage `?tab=link-person`。
|
||||
2. **新增**:`chapters.bound_super_user_id`;管理端章节绑定超级个体;读文接口返回绑定信息。
|
||||
3. **Webhook**:仅**白名单**埋点(分享/朋友圈/购买/单页解锁引导 + 补 `view_chapter`);`MiniprogramTrackPost` 后推送,使用 `loadLeadWebhookURL`,**独立去重**勿与获客日去重混用。
|
||||
4. **留资**:`ckb/lead` 按章节绑定合并 `TargetMemberID`,新增来源文案。
|
||||
|
||||
用户已选推送策略:**白名单关键事件**(非全量节流、非定时摘要)。
|
||||
|
||||
---
|
||||
|
||||
## 补充:找伙伴(终身 1 次免费 + ¥9.9 划线 / ¥1)
|
||||
|
||||
- **规则与 UI**:后端 `match.go` 与小程序 `match` 页已基本实现「终身 1 次免费、不按天重置」及划线原价 + 现价展示。
|
||||
- **支付对齐**:`getStandardPrice(match)` 当前误读 `chapter_config` 且兜底 **¥68**,需改为读 **`match_config.matchPrice`**、兜底 **¥1**(详见 Cursor 主计划文末章节与 todo `match-quota-pay-align`)。
|
||||
|
||||
- **#标签·内部跳转**:与现有 `type=internal`(管理端现名「本小程序页面」)为同一能力;执行时可将文案统一为「内部跳转」(todo `linktag-internal-label`)。
|
||||
|
||||
- **行为/Webhook 中文化**:飞书「最近行为」里英文 action、英文 module、技术 target 串,需在 `user.go` 补映射与人话化 target(todo `track-webhook-cn-labels`)。
|
||||
17
.cursor/rules/智能体-先查经验-技能索引.mdc
Normal file
17
.cursor/rules/智能体-先查经验-技能索引.mdc
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
description: 所有智能体处理任务前先查经验与技能索引
|
||||
globs: ["**"]
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 强制流程:先查经验与技能再处理
|
||||
|
||||
当任何智能体接到“问题/需求/变更/执行”任务时,必须按以下顺序完成(不能跳过):
|
||||
|
||||
1. 归类角色:根据编辑目录或触发词判断可能涉及的角色/任务类型。
|
||||
2. 查总索引:先打开 `.cursor/agent/智能体-技能-经验总索引.md` 找匹配的“经验索引文件”和“主 Skill 文件”。
|
||||
3. 查经验+查 Skill:对每个匹配条目,先读对应角色 `agent/*/evolution/索引.md`,再按边界规则读取对应主 `skills/*/SKILL.md` 完整内容。
|
||||
4. 再综合落地:确认是否还需 `change-checklist`、`team-meeting`、`role-flow-control` 等跨层 Skill 后,再回答/执行。
|
||||
|
||||
注:本规则不替代现有 project boundary 的“必须 Read 主 Skill 完整内容”的约束,只是增加“先查索引再读 Skill”的前置步骤。
|
||||
|
||||
@@ -12,8 +12,8 @@ const DEFAULT_MCH_ID = '1318592501'
|
||||
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
|
||||
// baseUrl 手动切换(注释方式):
|
||||
// const API_BASE_URL = 'http://localhost:9090'
|
||||
// const API_BASE_URL = 'https://soulapi.quwanzhi.com'
|
||||
const API_BASE_URL = 'https://souldev.quwanzhi.com'
|
||||
const API_BASE_URL = 'https://soulapi.quwanzhi.com'
|
||||
// const API_BASE_URL = 'https://souldev.quwanzhi.com'
|
||||
|
||||
const CONFIG_CACHE_KEY = 'mpConfigCacheV1'
|
||||
/** 新用户未完成头像+昵称前,强制留在资料引导(与 avatar-nickname 页配合) */
|
||||
|
||||
@@ -71,7 +71,8 @@ function getContentParseConfig() {
|
||||
aliases: p.aliases != null ? String(p.aliases) : '',
|
||||
}))
|
||||
const linkTags = Array.isArray(g.linkTagsConfig) ? g.linkTagsConfig : []
|
||||
return { persons, linkTags }
|
||||
const assetBase = String(g.baseUrl || '').replace(/\/$/, '')
|
||||
return { persons, linkTags, assetBase }
|
||||
}
|
||||
|
||||
/** 补全 mentionDisplay,避免旧数据无字段;昵称去空白防「@ 名」 */
|
||||
|
||||
@@ -27,6 +27,35 @@ function decodeEntities(str) {
|
||||
.replace(/'/g, "'")
|
||||
}
|
||||
|
||||
/**
|
||||
* 正文里 img 的 src:管理端常见为 /uploads/...(相对 API 根),浏览器会自动补全域名;
|
||||
* 小程序 <image> 不会拼接 baseUrl,需在此用 assetBase 转成可请求的绝对地址。
|
||||
* @param {string} src
|
||||
* @param {string} [assetBase] - 如 https://soulapi.example.com(无末尾 /)
|
||||
*/
|
||||
function resolveArticleImageSrc(src, assetBase) {
|
||||
if (!src || typeof src !== 'string') return src
|
||||
const s = src.trim()
|
||||
if (!s) return s
|
||||
if (/^(https?:|wxfile:|data:|blob:)/i.test(s)) return s
|
||||
if (s.startsWith('//')) return 'https:' + s
|
||||
if (s.startsWith('/')) {
|
||||
const base = String(assetBase || '').replace(/\/$/, '')
|
||||
if (!base) return s
|
||||
return base + s
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
/** 从 <img ...> 标签取出 src(双引号/单引号) */
|
||||
function extractImgSrcFromTag(tag) {
|
||||
const d = tag.match(/src\s*=\s*"([^"]*)"/i)
|
||||
if (d && d[1] != null) return d[1]
|
||||
const q = tag.match(/src\s*=\s*'([^']*)'/i)
|
||||
if (q && q[1] != null) return q[1]
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 单行展示用:昵称、#标签文案、章节外标题类字段 — 合并换行、<br>、连续空白(避免 TipTap/粘贴带入异常断行)
|
||||
*/
|
||||
@@ -118,11 +147,16 @@ function parseBlockToSegments(block, config) {
|
||||
segs.push({ type: 'linkTag', label: label || '#', url, tagType: '', pagePath: '', tagId: '' })
|
||||
|
||||
} else if (/^<img /i.test(tag)) {
|
||||
// 图片
|
||||
const srcMatch = tag.match(/src="([^"]*)"/)
|
||||
const altMatch = tag.match(/alt="([^"]*)"/)
|
||||
if (srcMatch) {
|
||||
segs.push({ type: 'image', src: srcMatch[1], alt: altMatch ? altMatch[1] : '' })
|
||||
// 图片(src 可能为相对路径,需结合 config.assetBase)
|
||||
const rawSrc = extractImgSrcFromTag(tag)
|
||||
const altMatch = tag.match(/alt\s*=\s*"([^"]*)"/i) || tag.match(/alt\s*=\s*'([^']*)'/i)
|
||||
if (rawSrc) {
|
||||
const decoded = decodeEntities(rawSrc)
|
||||
const src =
|
||||
config && config.assetBase
|
||||
? resolveArticleImageSrc(decoded, config.assetBase)
|
||||
: resolveArticleImageSrc(decoded, '')
|
||||
segs.push({ type: 'image', src, alt: altMatch ? decodeEntities(altMatch[1]) : '' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +173,7 @@ function parseBlockToSegments(block, config) {
|
||||
/**
|
||||
* 从 HTML 中解析出 lines(纯文本行)和 segments(含富文本片段)
|
||||
* @param {string} html
|
||||
* @param {object} [config] - { persons: [], linkTags: [] },用于对 text 段自动匹配 @人名 / #标签
|
||||
* @param {object} [config] - { persons: [], linkTags: [], assetBase?: string },用于对 text 段自动匹配 @人名 / #标签;assetBase 用于补全图片相对 URL
|
||||
*/
|
||||
function parseHtmlToSegments(html, config) {
|
||||
const lines = []
|
||||
|
||||
File diff suppressed because one or more lines are too long
1
soul-admin/dist/assets/index-CuUAjfcM.css
vendored
1
soul-admin/dist/assets/index-CuUAjfcM.css
vendored
File diff suppressed because one or more lines are too long
1
soul-admin/dist/assets/index-Gzc8q2lr.css
vendored
Normal file
1
soul-admin/dist/assets/index-Gzc8q2lr.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
soul-admin/dist/index.html
vendored
4
soul-admin/dist/index.html
vendored
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理后台 - Soul创业派对</title>
|
||||
<script type="module" crossorigin src="/assets/index-zk0czXxa.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CuUAjfcM.css">
|
||||
<script type="module" crossorigin src="/assets/index-C_CXp_Jv.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Gzc8q2lr.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -19,7 +19,6 @@ import { MentorsPage } from './pages/mentors/MentorsPage'
|
||||
import { MentorConsultationsPage } from './pages/mentor-consultations/MentorConsultationsPage'
|
||||
import { FindPartnerPage } from './pages/find-partner/FindPartnerPage'
|
||||
import { ApiDocsPage } from './pages/api-docs/ApiDocsPage'
|
||||
import { OpenPlatformPage } from './pages/open-platform/OpenPlatformPage'
|
||||
import { NotFoundPage } from './pages/not-found/NotFoundPage'
|
||||
|
||||
function App() {
|
||||
@@ -49,7 +48,7 @@ function App() {
|
||||
<Route path="match-records" element={<MatchRecordsPage />} />
|
||||
<Route path="api-doc" element={<Navigate to="/api-docs" replace />} />
|
||||
<Route path="api-docs" element={<ApiDocsPage />} />
|
||||
<Route path="open-platform" element={<OpenPlatformPage />} />
|
||||
<Route path="open-platform" element={<Navigate to="/settings?tab=open-platform" replace />} />
|
||||
</Route>
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Wallet,
|
||||
BookOpen,
|
||||
GitMerge,
|
||||
Globe2,
|
||||
} from 'lucide-react'
|
||||
import { get, post } from '@/api/client'
|
||||
import { clearAdminToken, getAdminToken } from '@/api/auth'
|
||||
@@ -123,17 +122,6 @@ export function AdminLayout() {
|
||||
<Settings className="w-5 h-5 shrink-0" />
|
||||
<span className="text-sm">系统设置</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/open-platform"
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
|
||||
location.pathname === '/open-platform'
|
||||
? 'bg-[#38bdac]/20 text-[#38bdac] font-medium'
|
||||
: 'text-gray-400 hover:bg-gray-700/50 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Globe2 className="w-5 h-5 shrink-0" />
|
||||
<span className="text-sm">API开放平台</span>
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -592,12 +592,24 @@ export function ContentPage() {
|
||||
isPinned?: boolean
|
||||
}
|
||||
type VipRow = { id: string; webhookUrl?: string | null }
|
||||
const [data, vipRes] = await Promise.all([
|
||||
const [data, vipRes, whCfg] = await Promise.all([
|
||||
get<{ success?: boolean; persons?: PersonResp[] }>('/api/db/persons'),
|
||||
get<{ success?: boolean; data?: VipRow[]; error?: string }>('/api/db/vip-members?limit=500').catch(() => null),
|
||||
get<{ success?: boolean; data?: Record<string, unknown> }>(
|
||||
'/api/db/config/full?key=super_individual_webhook_map',
|
||||
{ cache: 'no-store' as RequestCache },
|
||||
).catch(() => null),
|
||||
])
|
||||
const whMap: Record<string, string> = {}
|
||||
if (vipRes?.success && Array.isArray(vipRes.data)) {
|
||||
// 与 DBVipMemberWebhookSet 一致:完整映射存在 system_config,勿仅从 vip-members 列表合并
|
||||
// (列表仅含当前有效 VIP,过期会员绑定的人物会导致弹窗空白)
|
||||
if (whCfg?.success && whCfg.data && typeof whCfg.data === 'object' && !Array.isArray(whCfg.data)) {
|
||||
for (const [k, v] of Object.entries(whCfg.data)) {
|
||||
const uid = String(k || '').trim()
|
||||
const w = v != null ? String(v).trim() : ''
|
||||
if (uid && w) whMap[uid] = w
|
||||
}
|
||||
} else if (vipRes?.success && Array.isArray(vipRes.data)) {
|
||||
for (const m of vipRes.data) {
|
||||
const uid = (m.id || '').trim()
|
||||
if (!uid) continue
|
||||
@@ -2826,7 +2838,7 @@ export function ContentPage() {
|
||||
文章 @ 存 <span className="text-gray-400">token</span>;小程序点 @ 用 token 换存客宝密钥后加好友/拉群。
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-purple-300">超级个体</span>:开通且昵称合法会自动进本列表可 @;共用「超级个体拉群」统一获客计划(话术由技术侧在系统里配置,本页不填 plan/apiKey,避免误操作)。
|
||||
<span className="text-purple-300">超级个体</span>:与「用户管理 → 超级个体列表」同口径的有效 VIP;每次打开/刷新本页时,后端会为尚未出现在本表的用户自动补一条可 @ 人物(昵称须合法、存客宝可用)。已配置统一计划时走「超级个体同步」获客计划。
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-[#38bdac]">飞书群 Webhook</span>(按人推线索到群):在下方表格「飞书群」列查看状态,操作栏点 Webhook 图标配置;与绑定会员用户维度一致。
|
||||
|
||||
@@ -390,8 +390,19 @@ export function DashboardPage() {
|
||||
const t = target.trim()
|
||||
const lower = t.toLowerCase()
|
||||
|
||||
// 首页卡片:trackClick('home','card_click','超级个体_'+userId) — 中文前缀,不能依赖 lower.includes('super')
|
||||
if (/^超级个体[_::]/.test(t)) {
|
||||
const raw = t.replace(/^超级个体[_::]/, '').trim()
|
||||
const vipName = resolveVipNameByToken(raw) || resolveVipNameByToken(normalizeTrackToken(raw))
|
||||
if (vipName) return `超级个体:${vipName}`
|
||||
return `超级个体:${normalizeTrackToken(raw) || raw || '未知'}`
|
||||
}
|
||||
|
||||
if (/^链接头像[_-]/.test(t)) {
|
||||
const rawName = normalizeTrackToken(t.replace(/^链接头像[_-]/, ''))
|
||||
const rawId = t.replace(/^链接头像[_-]/, '').trim()
|
||||
const nick = resolveVipNameByToken(rawId) || resolveVipNameByToken(normalizeTrackToken(rawId))
|
||||
if (nick) return `头像:${nick}`
|
||||
const rawName = normalizeTrackToken(rawId)
|
||||
return rawName ? `头像:${rawName}` : '头像点击'
|
||||
}
|
||||
if (/^member[_-]?detail$/i.test(lower) || lower.includes('member detail')) return '成员详情'
|
||||
|
||||
@@ -8,9 +8,15 @@ import { OpenPlatformKeysTab } from './OpenPlatformKeysTab'
|
||||
import { OpenPlatformDocsTab } from './OpenPlatformDocsTab'
|
||||
import { OpenPlatformLogsTab } from './OpenPlatformLogsTab'
|
||||
|
||||
export function OpenPlatformPage() {
|
||||
export function OpenPlatformPage({ embedded = false }: { embedded?: boolean }) {
|
||||
return (
|
||||
<div className="p-8 w-full bg-[#0a1628] text-white min-h-full">
|
||||
<div
|
||||
className={
|
||||
embedded
|
||||
? 'w-full text-white'
|
||||
: 'p-8 w-full bg-[#0a1628] text-white min-h-full'
|
||||
}
|
||||
>
|
||||
<div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">API 开放平台</h1>
|
||||
@@ -20,7 +26,7 @@ export function OpenPlatformPage() {
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/api-docs"
|
||||
to="/settings?tab=api-docs"
|
||||
className="text-sm text-[#38bdac] hover:underline inline-flex items-center gap-1 shrink-0"
|
||||
>
|
||||
<Link2 className="w-4 h-4" />
|
||||
|
||||
@@ -41,11 +41,13 @@ import {
|
||||
LayoutGrid,
|
||||
Sparkles,
|
||||
MessageSquare,
|
||||
Globe2,
|
||||
} from 'lucide-react'
|
||||
import { get, post } from '@/api/client'
|
||||
import { AuthorSettingsPage } from '@/pages/author-settings/AuthorSettingsPage'
|
||||
import { AdminUsersPage } from '@/pages/admin-users/AdminUsersPage'
|
||||
import { ApiDocsPage } from '@/pages/api-docs/ApiDocsPage'
|
||||
import { OpenPlatformPage } from '@/pages/open-platform/OpenPlatformPage'
|
||||
import { MpUiPopupTableSection } from '@/pages/settings/MpUiPopupTableSection'
|
||||
import type { PagePopupItem } from '@/pages/settings/mpUiCopyConfig'
|
||||
import { buildMpUiPayload, splitMpUiForPopupEditor } from '@/pages/settings/mpUiCopyConfig'
|
||||
@@ -123,7 +125,7 @@ const defaultFeatures: FeatureConfig = {
|
||||
aboutEnabled: true,
|
||||
}
|
||||
|
||||
const TAB_KEYS = ['system', 'author', 'admin', 'api-docs'] as const
|
||||
const TAB_KEYS = ['system', 'author', 'admin', 'open-platform', 'api-docs'] as const
|
||||
type TabKey = (typeof TAB_KEYS)[number]
|
||||
|
||||
const SYSTEM_SECTION_KEYS = ['basic', 'mp', 'mp-copy', 'oss', 'features'] as const
|
||||
@@ -368,6 +370,13 @@ export function SettingsPage() {
|
||||
<ShieldCheck className="w-4 h-4 mr-2" />
|
||||
管理员
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="open-platform"
|
||||
className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-gray-400 data-[state=active]:font-medium"
|
||||
>
|
||||
<Globe2 className="w-4 h-4 mr-2" />
|
||||
API 开放平台
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="api-docs"
|
||||
className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-gray-400 data-[state=active]:font-medium"
|
||||
@@ -956,6 +965,10 @@ export function SettingsPage() {
|
||||
<AdminUsersPage />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="open-platform" className="mt-0">
|
||||
<OpenPlatformPage embedded />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="api-docs" className="mt-0">
|
||||
<ApiDocsPage />
|
||||
</TabsContent>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
soulApp (soul-api) Go 项目一键部署到宝塔(正式环境)
|
||||
soulApi(soul-api 后端)Go 项目一键部署到宝塔(正式环境)
|
||||
- 打包使用 .env.production 作为服务器 .env
|
||||
- 本地交叉编译 Linux 二进制
|
||||
- 上传到 /www/wwwroot/self/soul-api
|
||||
@@ -322,7 +322,7 @@ def upload_and_extract(cfg, tarball_path, no_restart=False, restart_method="auto
|
||||
print(" [成功] 已解压到: %s" % project_path)
|
||||
|
||||
if not no_restart:
|
||||
print("[4/4] 重启 soulApp 服务 ...")
|
||||
print("[4/4] 重启 soulApi 服务 ...")
|
||||
ok = False
|
||||
if restart_method in ("auto", "btapi") and (cfg.get("bt_panel_url") and cfg.get("bt_api_key")):
|
||||
ok = restart_via_bt_api(cfg)
|
||||
@@ -342,7 +342,7 @@ def upload_and_extract(cfg, tarball_path, no_restart=False, restart_method="auto
|
||||
print(" [stderr] %s" % err[:200])
|
||||
ok = "RESTART_OK" in out
|
||||
if ok:
|
||||
print(" [成功] soulApp 已通过 SSH 重启")
|
||||
print(" [成功] soulApi 已通过 SSH 重启")
|
||||
else:
|
||||
print(" [警告] SSH 重启状态未知,请到宝塔 Go 项目里手动点击启动,或执行: cd %s && ./soul-api" % project_path)
|
||||
else:
|
||||
@@ -368,7 +368,7 @@ def upload_and_extract(cfg, tarball_path, no_restart=False, restart_method="auto
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="soulApp (soul-api) Go 项目一键部署到宝塔",
|
||||
description="soulApi(soul-api 后端)Go 项目一键部署到宝塔",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--no-build", action="store_true", help="跳过本地编译(使用已有 soul-api 二进制)")
|
||||
@@ -387,7 +387,7 @@ def main():
|
||||
|
||||
cfg = get_cfg()
|
||||
print("=" * 60)
|
||||
print(" soulApp 一键部署到宝塔")
|
||||
print(" soulApi(soul-api)一键部署到宝塔")
|
||||
print("=" * 60)
|
||||
print(" 服务器: %s@%s:%s" % (cfg["user"], cfg["host"], DEFAULT_SSH_PORT))
|
||||
print(" 目标目录: %s" % cfg["project_path"])
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -108,10 +109,52 @@ func invalidateReadExtrasCache() {
|
||||
cache.Del(context.Background(), cache.KeyConfigReadExtras)
|
||||
}
|
||||
|
||||
// syncMissingSuperIndividualsAsPersons 将当前有效超级个体(VIP 口径与 vip-members 一致)中尚未绑定 persons.user_id 的用户,
|
||||
// 通过 ensurePersonForUser 追加为可 @ 人物(昵称不合法或存客宝失败时跳过并打日志)。
|
||||
func syncMissingSuperIndividualsAsPersons(db *gorm.DB) {
|
||||
ids := activeSuperIndividualUserIDs(db)
|
||||
if len(ids) == 0 {
|
||||
return
|
||||
}
|
||||
var bound []string
|
||||
if err := db.Model(&model.Person{}).
|
||||
Where("user_id IS NOT NULL AND user_id != ?", "").
|
||||
Distinct("user_id").
|
||||
Pluck("user_id", &bound).Error; err != nil {
|
||||
log.Printf("[persons] sync: list bound user_id failed: %v", err)
|
||||
return
|
||||
}
|
||||
boundSet := make(map[string]struct{}, len(bound))
|
||||
for _, b := range bound {
|
||||
boundSet[strings.TrimSpace(b)] = struct{}{}
|
||||
}
|
||||
any := false
|
||||
for _, uid := range ids {
|
||||
uid = strings.TrimSpace(uid)
|
||||
if uid == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := boundSet[uid]; ok {
|
||||
continue
|
||||
}
|
||||
if err := ensurePersonForUser(db, uid); err != nil {
|
||||
log.Printf("[persons] sync super-individual userId=%s: %v", uid, err)
|
||||
continue
|
||||
}
|
||||
boundSet[uid] = struct{}{}
|
||||
any = true
|
||||
}
|
||||
if any {
|
||||
invalidateReadExtrasCache()
|
||||
}
|
||||
}
|
||||
|
||||
// DBPersonList GET /api/db/persons 管理端-@提及人物列表
|
||||
func DBPersonList(c *gin.Context) {
|
||||
db := database.DB()
|
||||
syncMissingSuperIndividualsAsPersons(db)
|
||||
var rows []model.Person
|
||||
if err := database.DB().Order("name ASC").Find(&rows).Error; err != nil {
|
||||
if err := db.Order("name ASC").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -151,6 +151,37 @@ func DBVipMembersList(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list, "total": len(list)})
|
||||
}
|
||||
|
||||
// activeSuperIndividualUserIDs 与 DBVipMembersList / 小程序超级个体口径一致的有效用户 id(无条数上限,供 @ 列表自动补人)
|
||||
func activeSuperIndividualUserIDs(db *gorm.DB) []string {
|
||||
var users []model.User
|
||||
err := db.Table("users").
|
||||
Select("id").
|
||||
Where("is_vip = 1 AND vip_expire_date > ?", time.Now()).
|
||||
Order("id ASC").
|
||||
Find(&users).Error
|
||||
|
||||
if err != nil || len(users) == 0 {
|
||||
var userIDs []string
|
||||
db.Model(&model.Order{}).Select("DISTINCT user_id").
|
||||
Where("(status = ? OR status = ?) AND (product_type = ? OR product_type = ?)", "paid", "completed", "fullbook", "vip").
|
||||
Pluck("user_id", &userIDs)
|
||||
if len(userIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := db.Select("id").Where("id IN ?", userIDs).Find(&users).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
out := make([]string, 0, len(users))
|
||||
for _, u := range users {
|
||||
id := strings.TrimSpace(u.ID)
|
||||
if id != "" {
|
||||
out = append(out, id)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// DBVipMemberWebhookSet PUT /api/db/vip-members/webhook
|
||||
// 按超级个体用户维度配置飞书群 webhook(VOX 地址)。
|
||||
func DBVipMemberWebhookSet(c *gin.Context) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
soulApp (soul-api) Go 项目一键部署到宝塔(正式环境)
|
||||
soulApi(soul-api 后端)Go 项目一键部署到宝塔(正式环境)
|
||||
- 打包使用 .env.production 作为服务器 .env
|
||||
- 本地交叉编译 Linux 二进制
|
||||
- 上传到 /www/wwwroot/self/soul-api
|
||||
@@ -336,7 +336,7 @@ def upload_and_extract(cfg, tarball_path, no_restart=False, restart_method="auto
|
||||
print(" [成功] 已解压到: %s" % project_path)
|
||||
|
||||
if not no_restart:
|
||||
print("[4/4] 重启 soulApp 服务 ...")
|
||||
print("[4/4] 重启 soulApi 服务 ...")
|
||||
ok = False
|
||||
if restart_method in ("auto", "btapi") and (cfg.get("bt_panel_url") and cfg.get("bt_api_key")):
|
||||
ok = restart_via_bt_api(cfg)
|
||||
@@ -371,7 +371,7 @@ def upload_and_extract(cfg, tarball_path, no_restart=False, restart_method="auto
|
||||
out = stdout.read().decode("utf-8", errors="replace").strip()
|
||||
ok = "RESTART_OK" in out
|
||||
if ok:
|
||||
print(" [成功] soulApp 已通过 SSH 重启")
|
||||
print(" [成功] soulApi 已通过 SSH 重启")
|
||||
else:
|
||||
print(" [警告] SSH 重启状态未知,请到宝塔 Go 项目里手动点击启动,或执行: cd %s && ./soul-api" % project_path)
|
||||
else:
|
||||
@@ -397,7 +397,7 @@ def upload_and_extract(cfg, tarball_path, no_restart=False, restart_method="auto
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="soulApp (soul-api) Go 项目一键部署到宝塔",
|
||||
description="soulApi(soul-api 后端)Go 项目一键部署到宝塔",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--no-build", action="store_true", help="跳过本地编译(使用已有 soul-api 二进制)")
|
||||
@@ -416,7 +416,7 @@ def main():
|
||||
|
||||
cfg = get_cfg()
|
||||
print("=" * 60)
|
||||
print(" soulApp 一键部署到宝塔")
|
||||
print(" soulApi(soul-api)一键部署到宝塔")
|
||||
print("=" * 60)
|
||||
print(" 服务器: %s@%s:%s" % (cfg["user"], cfg["host"], DEFAULT_SSH_PORT))
|
||||
print(" 目标目录: %s" % cfg["project_path"])
|
||||
|
||||
Reference in New Issue
Block a user