工作台:目录改名 mcn-workshop + 任务进度可见 + 会话归属配置化
1. 目录 mcn-work-shop -> mcn-workshop(空间分组名取会话 cwd 的目录名,只改字符串无效,必须真改名) 2. 全仓替换 mcn-work-shop -> mcn-workshop:37 文件 121 处;历史日志按沿革句规矩保留当时目录名 3. 任务进度可见:/api/run/status 产出 已运行时长/工具调用/最近动作/停滞判定(读会话日志尾部),前端新增右下角常驻面板,四处任务入口接入 4. 会话归属目录配置化:新增 config.sessionCwd(留空=工作台自身目录),现指向 D://AI技能//mcn-workshop,使任务显示为命名分组而非「未分组任务」 5. 修前端轮询静默缺陷:连续 3 次查询失败即提示服务断开(原逻辑静默空转到 15 分钟超时,用户零感知)
This commit is contained in:
1 parent
d07049ecf1
commit
f77735b206
115 files changed
+1103
-1514
No files matched your search
+2
-2
@@ -9,8 +9,8 @@ Thumbs.db
|
|||||||
# WorkBuddy 自动化任务运行数据(本地运行时生成,不提交)
|
# WorkBuddy 自动化任务运行数据(本地运行时生成,不提交)
|
||||||
.workbuddy/automations/
|
.workbuddy/automations/
|
||||||
# 工作台运行日志与 CDP 自检截图(本地产物,不提交)
|
# 工作台运行日志与 CDP 自检截图(本地产物,不提交)
|
||||||
**/mcn-work-shop/server.log
|
**/mcn-workshop/server.log
|
||||||
**/mcn-work-shop/shot-*.png
|
**/mcn-workshop/shot-*.png
|
||||||
# 工作台本地数据库(含账号真实数据,不入库;根目录 0 字节残留同规则)
|
# 工作台本地数据库(含账号真实数据,不入库;根目录 0 字节残留同规则)
|
||||||
mcn-plugin.db
|
mcn-plugin.db
|
||||||
**/mcn-plugin.db
|
**/mcn-plugin.db
|
||||||
|
|||||||
@@ -368,3 +368,150 @@ codebuddy -p → EADDRINUSE 后卡死,超时零输出
|
|||||||
|
|
||||||
### 清理
|
### 清理
|
||||||
本轮 91 个临时文件(`_*.py`/`_*.log`/`_*.txt`/`_debug/` 等,均在根目录与 mcn-work-shop 下)已全部分批删除。
|
本轮 91 个临时文件(`_*.py`/`_*.log`/`_*.txt`/`_debug/` 等,均在根目录与 mcn-work-shop 下)已全部分批删除。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 18:45-19:30 A 方案落地完成 + 联网搜索接入(实测全通过)
|
||||||
|
|
||||||
|
### ✅ 结果:工作台 AI 链路已切到本地 CLI,可用
|
||||||
|
用户提供 API Key 后,端到端全部跑通:
|
||||||
|
`/api/ai/status` → ready:true / `?probe=1` → ok:true / `/api/ai/clarify`(SSE) → 正常出内容且格式合规(`==OPTS==/==REQ==` 完整)。
|
||||||
|
|
||||||
|
### 🔐 凭据存放(重要,防泄密)
|
||||||
|
- **`mcn-work-shop/config.json` 是 git 跟踪文件** → ⛔ 绝不能把 key 写进去。
|
||||||
|
- 凭据落在**仓库之外**:`%USERPROFILE%\.workbuddy\mcn-work-shop\ai-cli.json`(`{"apiKey":"..."}`)。
|
||||||
|
- 解析优先级:`config.apiKey/authToken` > **凭据文件** > 环境变量 `CODEBUDDY_API_KEY/AUTH_TOKEN`。
|
||||||
|
- `server.js` 新增 `cliBaseOpts()` 统一构造调用参数;新增环境变量覆盖开关 **`MCN_AI_MODEL`**(不改配置做模型 A/B)。
|
||||||
|
|
||||||
|
### ⭐⭐ doubao 不可用(钉死结论)
|
||||||
|
- `custom:doubao-seed-2-1-pro-260628` → **500**;`custom:doubao-seed-2-0-lite-260428` → **400 `Custom model [...] service info not found`**。
|
||||||
|
- CLI `--help` 的模型列表是**二进制静态清单,≠ 已开通**。桌面产品配置 48 个模型里没 doubao;`workbuddy.db` 历史只用过 `deepseek-v4.1-flash`/`hy4-preview`。
|
||||||
|
- **但 `custom:` 系并非全不可用** —— 同租户下有一大批已开通的自定义槽位。
|
||||||
|
|
||||||
|
### ⭐⭐ `custom:custom-model-*` 槽位 → 真实模型名映射(实测逐个跑出)
|
||||||
|
| 槽位 | 真名 |
|
||||||
|
|:--|:--|
|
||||||
|
| `a1` / `a2` / `a3` / `a4` | Claude-Opus-4.7 / Sonnet-4.6 / Opus-4.6 / **Opus-4.8** |
|
||||||
|
| `b1-standard` / `b1-priority` | GPT-5.4-Standard / GPT-5.4-Priority |
|
||||||
|
| `b3-priority` | GPT-5.2-Priority |
|
||||||
|
| `b4-standard` / `b4-priority` | GPT-5.1-Standard / GPT-5.1-Priority |
|
||||||
|
| `b5-standard` / `b5-priority` | **GPT-5.5-Standard / GPT-5.5-Priority** |
|
||||||
|
| `c2-flash` | Gemini-3.5-Flash-1 |
|
||||||
|
|
||||||
|
> **取证方法**(可复用):CLI 日志 `%TEMP%\mcn-cli-home\logs\<日期>\*.log` 里会有
|
||||||
|
> `"requestModelId":"custom:...","requestModelName":"Claude-Opus-4.8"` 配对 → 正则一次提取全部映射。
|
||||||
|
> 直接问模型「你是谁」不可靠(a1~a4 会统一答「我是AI智能编程助手」)。
|
||||||
|
|
||||||
|
### ⭐⭐ 三个性能杠杆(实测数据,都已在代码里落地)
|
||||||
|
| 杠杆 | 做法 | 效果 |
|
||||||
|
|:--|:--|:--|
|
||||||
|
| **工具面收敛** | `--tools "WebSearch,WebFetch"` | prompt **21,776 → 4,859 tokens(-78%)**,**47.6s → 15.0s** |
|
||||||
|
| **推理档位** | `--effort low`(新增,`minimal\|low\|medium\|high\|xhigh\|max`) | 首字 **61.7s → 19.7s** |
|
||||||
|
| **换模型** | `deepseek-v4-pro` 46.2s → `custom:custom-model-a4` 18.5s | 快 2.5 倍 |
|
||||||
|
|
||||||
|
- ⚠️ **`[INIT tools]` 事件列的是工具注册表全量,不是启用集** —— 判断 `--tools` 是否生效要看 `usage.input_tokens`,别被事件列表误导(本轮差点误判成"白名单没生效")。
|
||||||
|
- 附带安全收益:工具面收敛后模型**物理上碰不到文件系统**(Bash/Read/Write/Edit 全不在白名单)。
|
||||||
|
|
||||||
|
### ⭐ 联网搜索:CLI 原生自带,开箱可用
|
||||||
|
- `--tools` 里保留 `WebSearch,WebFetch` 即可;实测真的发起 `WebSearch {"query":...,"freshness":"d1"}`。
|
||||||
|
- **但不能常开也不能常关** → 用 `CLI_SEARCH_HINT` 作 `--append-system-prompt` 注入判据:
|
||||||
|
该搜(今天/近期的外部事实、热点、平台规则、事实核查)/不该搜(创作讨论、需求澄清、脚本撰写)/
|
||||||
|
**搜索预算硬上限 2 次**(1 WebSearch + 必要 1 WebFetch),搜到即答禁止反复搜。
|
||||||
|
- 教训:第一版提示词只写"最多搜 2 轮",模型实际连搜 4 次把耗时推到 3 分钟 → 改成显式预算 + `maxTurns: 6` 硬顶。
|
||||||
|
|
||||||
|
### ⭐ 两个文本污染(必须处理)
|
||||||
|
1. **报错当回答**:未认证时 CLI 把报错走文本通道发出 → `isCliBoilerplate()` 拦截。
|
||||||
|
2. **工具调用前的过程话术**(本轮新发现):模型搜前先吐一句
|
||||||
|
`I'll search for ... before responding.` → 被当正文显示。
|
||||||
|
**修法**:`runCli` 记录每次 `tool_use` 的位置(`toolMarks`),**只保留最后一次工具调用之后的文本**;
|
||||||
|
因为这段已流式推给前端,`server.js` 成功分支比对 `r.text !== acc` 时补发 `{replace}` 整体覆盖。
|
||||||
|
(前端 `data-pages.js` L653-672 对未知事件类型是 `continue` 安全跳过 → 加事件类型不破坏兼容。)
|
||||||
|
|
||||||
|
### ⭐ 本轮新识别并清掉的宿主干扰
|
||||||
|
- **宿主 MCP 注入**:`CODEBUDDY_MCP_CONFIG` 会在子 CLI 里自动挂上 `weixinpay` + `sheetagent` 两个 MCP,
|
||||||
|
纯浪费启动时间还可能被误调 → `buildChildEnv` 删该变量 + 传 `--strict-mcp-config --mcp-config '{"mcpServers":{}}'`。
|
||||||
|
实测 `[INIT mcp]` 从 2 个变成 `[]`。
|
||||||
|
- 另删 `CODEBUDDY_GATEWAY_PASSWORD`/`CODEBUDDY_GATEWAY_AUTH`/`WORKBUDDY_PAC_RPC_*`。
|
||||||
|
|
||||||
|
### 当前配置
|
||||||
|
`config.json → ai`:`backend:"cli"`,`model:"custom:custom-model-a4"`(Claude-Opus-4.8),
|
||||||
|
`effort:"low"`,`search:true`,`tools:""`,`strictMcp:true`,`maxTurns:6`。
|
||||||
|
|
||||||
|
### 遗留 / 待办
|
||||||
|
- **需求打磨单轮耗时仍 60-120s**(首字已降到 ~20s)。总耗时主要在模型推理+输出,属模型特性;若要更快可再换 `b5-standard`(GPT-5.5) 或加 `effort:minimal`。
|
||||||
|
- **前端 `sendTurn` 未实测**(本轮只测了后端 SSE;协议未变理论零改动)。
|
||||||
|
- 真实账号 + 参考选题走**完整 AI 创作链路**(S1-S11)仍未验证。
|
||||||
|
- `flash`/`minimal` 档位对产出质量的影响未评估。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 19:32-19:40 模型切换:GPT-5.5(用户拍板)
|
||||||
|
|
||||||
|
用户选定 **GPT-5.5**,配置已改为 `custom:custom-model-b5-standard`。
|
||||||
|
|
||||||
|
### 实测对比(端到端 `/api/ai/clarify`,同一测试用例)
|
||||||
|
| 模型 | 普通创作问答 | 问热点(带搜索) | 格式合规 |
|
||||||
|
|:--|:--|:--|:--|
|
||||||
|
| **GPT-5.5-Standard**(当前) | **42.2s** | 137.2s | ✅ |
|
||||||
|
| Claude-Opus-4.8 | 105.5s | 139.6s | ✅ |
|
||||||
|
| DeepSeek-V4-Pro | 119.5s | 151.2s | ✅ |
|
||||||
|
|
||||||
|
- **普通问答快 2.5 倍**(42.2s vs 105.5s)—— 这是最常用的路径,收益最大。
|
||||||
|
- **触发搜索后各模型都是 130-150s** → 瓶颈转移到搜索本身,不是模型。
|
||||||
|
∴ `CLI_SEARCH_HINT` 把搜索预算压到 ≤2 次是对的,这是搜索场景唯一有效的提速手段。
|
||||||
|
- 首字延迟:普通问答 ~20s;搜索场景 ~67s(因为叙述被切掉了,搜索期间用户只看到"正在思考"占位)。
|
||||||
|
|
||||||
|
### b5 两个变体的区别
|
||||||
|
`custom:custom-model-b5-standard`(标准档,已选)/`custom:custom-model-b5-priority`(优先档,
|
||||||
|
资源紧张时优先路由,通常积分倍率更高)。b1/b3/b4 同族也是这套 -standard/-priority 后缀。
|
||||||
|
|
||||||
|
### 本轮验证方法(可复用)
|
||||||
|
测试脚本自检三件事,比只看耗时更有价值:
|
||||||
|
1. `==OPTS==` / `==REQ==` 双标记是否齐全(格式合规,前端要靠它解析候选与需求清单)
|
||||||
|
2. 英文过程话术残留(正则扫整行纯英文 ≥25 字符的段落)
|
||||||
|
3. 首字延迟 + 总耗时分开记(首字反映推理档位,总耗时反映输出+搜索)
|
||||||
|
|
||||||
|
### 当前最终配置
|
||||||
|
`config.json → ai`:`backend:"cli"`,`model:"custom:custom-model-b5-standard"`,
|
||||||
|
`effort:"low"`,`search:true`,`tools:""`,`strictMcp:true`,`maxTurns:6`;
|
||||||
|
凭据在 `%USERPROFILE%\.workbuddy\mcn-work-shop\ai-cli.json`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 19:40-19:50 切换 deepseek-v4.1-flash + 全流程跑通验证
|
||||||
|
|
||||||
|
用户要求「改为用 deepseek v4.1 flash 把流程跑通」。配置已改 `custom:custom-model-b5-standard` → **`deepseek-v4.1-flash`**。
|
||||||
|
|
||||||
|
### ✅ 全流程四环节实测全绿
|
||||||
|
| 环节 | 结果 |
|
||||||
|
|:--|:--|
|
||||||
|
| `GET /api/ai/status` | backend=cli · model=deepseek-v4.1-flash · ready=**true** |
|
||||||
|
| `?probe=1` | ok=**true** · 17.0s · 返回「成功」 |
|
||||||
|
| `POST /api/ai/clarify`(stream) | 371 字 · `==OPTS==/==REQ==` 合规 ✅ · 无英文残留 ✅ · 51.7s(首字 49.7s) |
|
||||||
|
| `POST /api/run` → 轮询 | queued(5s) → running(30s) → **done**(50s) · `model_id` 落库 = deepseek-v4.1-flash ✅ |
|
||||||
|
|
||||||
|
### ⭐⭐ 关键架构发现:完整流程 = 两段,模型来源互不相干
|
||||||
|
| 环节 | 接口 | 谁在跑 | 模型来源 |
|
||||||
|
|:--|:--|:--|:--|
|
||||||
|
| 需求打磨(弹窗对话) | `POST /api/ai/clarify` | **本地 CLI**(本方案) | `config.json → ai.cli.model` |
|
||||||
|
| 提交创作任务(S1-S11 生成脚本) | `POST /api/run` | **WorkBuddy 宿主**(写 automation,桌面端调度器执行) | **`sessions` 表最近活跃「手动会话」的 `model`**,兜底 `auto` |
|
||||||
|
|
||||||
|
- 🔴 **改 `ai.cli.model` 不影响创作任务的模型。**
|
||||||
|
- 创作任务模型是「跟随用户在 WorkBuddy UI 最近选过的模型」→ 从工作台侧看是**浮动的、不确定的**。
|
||||||
|
- 取证 SQL:`SELECT model_id, COUNT(*) FROM automations GROUP BY model_id ORDER BY 2 DESC;`
|
||||||
|
本机实测分布:`hy4-preview` **30** 条 / `deepseek-v4.1-flash` **6** 条(我们这次会话是 flash,所以新任务落到 flash)。
|
||||||
|
- **想让创作固定用某模型**:在 UI 把当前会话模型切到它;或改造 `/api/run` 支持显式 `model_id`(属行为变更,未做)。
|
||||||
|
|
||||||
|
### `deepseek-v4.1-flash` 的实测速度(澄清一个反直觉点)
|
||||||
|
- 首字 **49.7s**,总 51.7s;对比 GPT-5.5 首字 20.0s/总 42.2s。
|
||||||
|
- **"flash" 不等于首字更快** —— 它仍是推理型模型;但总耗时接近。选它主要看积分成本。
|
||||||
|
- 三个模型(deepseek-v4.1-flash / GPT-5.5 / Opus-4.8)**格式合规都 ✅、都无英文残留**,质量层面可用性一致。
|
||||||
|
|
||||||
|
### 顺带确认的死代码
|
||||||
|
- **`POST /api/ai/topics` 是遗留死代码** —— 前端从不调用(选题列表走 `GET /api/dsh/topics`)。
|
||||||
|
它仍写死 Dify,未配 `DIFY_MCN_CYLG_KEY` 会 500,但**不影响真实流程**。已记入文档 §11。
|
||||||
|
|
||||||
|
### 清理
|
||||||
|
- 自检 automation `automation-1790682160345`(名为「链路自检·deepseek-v4.1-flash(可删除)」)
|
||||||
|
已用 `automation_update mode=delete` 走正规通道删除(未用 sqlite 直改)。
|
||||||
|
- 临时脚本 `_t_flash.py` 已删。
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# 2026-10-08
|
||||||
|
|
||||||
|
## 会话技能加载 + 项目记忆去重
|
||||||
|
|
||||||
|
- 按用户指令加载 `session-mechanism`:四类必读已读(`00-动手前必过.md` / `rules.md` / `manifest.md` / `01-文档索引.md`)+ SKILL.md 第一屏(触发句、第 0 步加载门槛、十条禁令、自决策白名单、现状地图)。
|
||||||
|
- 内化判据:**边界内自决策**(技术选型/实现路径/命名/调参/部署/排查/版本/兼容降级/文档技术内容);**只提报三类**(功能语义分叉/红线门禁/超边界:花钱·对外承诺·要凭据)。
|
||||||
|
- 项目 `MEMORY.md` 去重重组为 v4(17,841 → 17,315 B):新增 §0 两条指针(工作台 AI 通路 `docs/AI链路-本地CLI接入.md`、技能侧 WeKnora 调用);§2.1 补 09-29 AI 通路四条硬红线+两段流程模型来源;§4.2/§4.3 降级为「存根+硬红线」;清历史流水。
|
||||||
|
|
||||||
|
### 未决
|
||||||
|
- ⚠️ `MEMORY.md` 体积仍会被注入截断(阈值约 10–12 KB)。解法二选一:① 维持单文件(细节自动可见,但注入截断)② 拆专题文件(不截断,但细节失去自动注入)。**无客观优劣 ⇒ 待用户定**。
|
||||||
|
|
||||||
|
## 工作台「更新榜单数据」按钮排查(功能正常,是可见性问题)
|
||||||
|
|
||||||
|
- 现象:用户点击后「没有会话执行」。
|
||||||
|
- 取证(四处读数):宿主库 `automations` 有该 once 任务(ACTIVE、`next_run_at` 已过期、`model=hy4-preview`、`skills=["mcn-data-insight"]`);`automation_runs.status=IN_PROGRESS` + `automation_runtime_state.running=1`(⚠️ `running_conversation_id=null` 是常态,不代表没起会话);`sessions` 已建后台会话 `8a085794…`(bg=1,10:07:23);会话 jsonl 283 KB 且 mtime 持续更新(10:14),工具调用 30 Bash/14 Read/4 Write ⇒ **任务真的在跑**。`/api/dsh/ranking-update` 返回 `files=104`、`lastUpdateAt` 10:13:51 ⇒ 数据已在落盘。
|
||||||
|
- 真因两条(都与抓取逻辑无关):① 会话 `cwd = …\mcn-work-shop` ⇒ 落在侧边栏 **mcn-work-shop 空间分组**,不在用户当前空间,需切组才可见(这是设计,非 bug);② 工作台服务作为会话后台任务**被回收** ⇒ 前端轮询 `/api/run/status` 持续抛错。
|
||||||
|
- 修复:`public/data-pages.js` `updateRankingData()` 轮询加失败计数 —— 连续 3 次查询失败即提示「工作台服务已断开,任务仍在后台执行」并结束轮询(原逻辑静默 `continue` 到 15 分钟超时,用户零感知)。
|
||||||
|
- ⚠️ **排查方法论沉淀**:判「后台任务有没有在执行」的正确取證链 = `automations` → `automation_runs` → `automation_runtime_state` → `sessions` → 会话 jsonl 的 mtime/大小。⛔ 别只看 `running_conversation_id`(常为 null,会误判成没执行)。
|
||||||
|
|
||||||
|
## 工作台目录统一改名 `mcn-work-shop` → `mcn-workshop`
|
||||||
|
|
||||||
|
- 动因(用户):「分组已改为 mcn-workshop,需要全部替换;执行会话任务时需要按分组名称显示在会话列表」。
|
||||||
|
🔑 **机制**:WorkBuddy 侧边栏空间分组名 = 会话 `cwd` 的 **basename** ⇒ **只改字符串无效,必须真改名目录**。
|
||||||
|
- 执行五步:① 停服务释放句柄(否则 rename 必 `WinError 5`)→ ② `mv mcn-work-shop mcn-workshop`(成功)→ ③ 临时脚本全仓替换 **`mcn-work-shop`→`mcn-workshop`:37 文件 / 121 处**;⛔ 按**沿革句规矩跳过** `.workbuddy/memory/YYYY-MM-DD.md`(16 个历史日志保留当时目录名史实)→ ④ 修 2 处被替换改坏语义的注释(`server.js` L1265 / L1269,原「旧 mcn-workshop/新建 mcn-workshop」被换成同词,已补回三代路径)→ ⑤ 重启验证:`app.js`/`data-pages.js` 200,`data-pages.js` 含 `pollFail` 修复。
|
||||||
|
- ⚠️ **事实澄清(易踩)**:`D:\AI技能\mcn-workshop` **不是工作台**,是 09-01 路径拼接 bug 留下的**垃圾目录**(3 个 0 字节文件 + `outputs`)。工作台本体一直在仓库内 `…\V1.0\mcn-workshop`。sessions 表取证:该垃圾目录下**零会话**;未删除会话中仅 1 条 mcn-work 相关,cwd 仍是旧 `…\mcn-work-shop`。
|
||||||
|
- 净效果:新建后台会话 cwd = `…\V1.0\mcn-workshop` ⇒ 侧边栏分组显示 **`mcn-workshop`** ✅
|
||||||
|
- 残留:`project/…/subskills/mcn-data-insight/scripts/__pycache__/*.pyc`(二进制缓存含旧串,会自行重生成,无需处理)。
|
||||||
|
|
||||||
|
### 追加澄清(用户真实意图,⛔ 别再理解成"目录改名")
|
||||||
|
- 用户要的不是改目录名,而是:**工作台提交 WorkBuddy 定时任务后,产生的 AI 会话应归属哪个「空间分组」**。
|
||||||
|
- 机制:分组名 = 会话 `cwd` 的**目录名**;工作台侧由 `server.js` 的 `SESSION_CWD`(当前 = 工作台自身目录)决定 ⇒ **改目录名只是让分组名变成 `mcn-workshop` 的手段**,不是目的本身。
|
||||||
|
- ⚠️ **存量不掉头**:10:07 那条榜单更新任务 cwd 仍是旧 `…\mcn-work-shop` ⇒ 显示在**旧**分组;改名后**新建**的任务才落到 `mcn-workshop`。清理用的 `SESSION_CWD_PATTERN='%mcn-work%'` 覆盖新旧三代,不受影响。
|
||||||
|
- 待用户定(功能语义分叉):① 维持工作台专属分组 ② 归入用户当前主项目分组 ③ 自定义独立分组(需目录真实存在且在 workspaces 登记,08-27 即此法)。可选项:把 `SESSION_CWD` 提升为 `config.json` 可配字段,换分组只改配置不动代码。
|
||||||
|
|
||||||
|
## 后台任务进度可见(用户:「我需要看到 才知道执行是否正常」)
|
||||||
|
|
||||||
|
- 真需求:不是分组放哪,而是**在工作台内直接看到执行状态**,判断在跑还是卡住、做了什么。
|
||||||
|
- 后端 `server.js` `/api/run/status` 增强:automation → 关联它拉起的后台会话(`sessions.is_background_automation=1` 且创建时间落在任务创建 −15s~+300s)→ 读该会话 jsonl **尾部 256 KB** → 新增字段
|
||||||
|
`elapsedMs` / `sessionId` / `sessionTitle` / `lastAction`(最近一段文本,截断 160 字)/ `toolCalls`(按工具名计数)/ `updatedAgoMs` / `stalled`(>90 s 无新输出)/ `logBytes`。
|
||||||
|
- ⭐ 会话日志路径换算(可复用):cwd → projects 目录名 = **盘符小写 + 分隔符全转 `-`**(`escapeCwdForProjects()`);基目录 `<workbuddy>/projects/<转义名>/<sessionId>.jsonl`。
|
||||||
|
- ⚠️ `stalled` 只在 `state=running` 时有意义(已完成任务日志当然不再写入,会恒 true)。
|
||||||
|
- 前端新增 `window.TaskProgress`(实现在 `public/app.js`,样式 `style.css` 的 `.tp-*`):右下角常驻面板 = 状态/已运行时长/工具调用/最近动作/停滞警告。**四处轮询点全接入**:`app.js executeTask`、`data-pages.js` 的榜单更新/批量更新账号/视频解析。终态保留 8 秒自动收起,运行超时则保留供继续观察。
|
||||||
|
- 验证:三文件 `node --check` 全过;服务起来后 `app.js` 含 6 处、`data-pages.js` 含 9 处 `TaskProgress`,`style.css` 含 `.tp-panel`;状态接口返回完整进度字段。
|
||||||
|
|
||||||
|
### ✅ 分组归属定案:就用 `mcn-workshop` 文件夹对应的分组(已实测)
|
||||||
|
- 用户拍板:任务会话归到 **`mcn-workshop` 文件夹对应的那个分组**。
|
||||||
|
- 实测(提交轻量自检任务取证):新会话 `cwd = …\V1.0\mcn-workshop` ✅;对照 10:07 那条旧榜单任务仍是 `…\mcn-work-shop`(**两者在侧边栏分属不同分组**,属正常存量,不用管)。
|
||||||
|
- 结论:**不需要再改代码** —— `SESSION_CWD = ROOT`,目录改名后自动生效。自检任务已删排期。
|
||||||
|
- ⚠️ 认知要点:侧边栏分组按 **完整 cwd** 聚合,不是按文件夹名合并 ⇒ 同名不同路径会是两个分组。
|
||||||
|
|
||||||
|
### ⚠️ 更正(12:2x):分组靠 cwd **命中已打开的工作区**,光改目录名不够
|
||||||
|
- 上一条只对了一半:目录改名后会话 cwd 是**仓库内** `…\V1.0\mcn-workshop`,**不是用户的工作区** ⇒ 仍显示在「**未分组任务**」区域(用户实测反馈)。
|
||||||
|
- 用户纠正:`D:\AI技能\mcn-workshop` **才是那个工作区**。我此前仅凭目录里只有 3 个 0 字节垃圾文件就判它"是垃圾目录"——**没查登记表就下结论**,违反「判状态先问程序自己」。
|
||||||
|
- 🔑 **真机制**:侧边栏分组 = 会话 cwd **命中已打开过的工作区**才显示为命名分组;没命中 ⇒ 落「未分组任务」。
|
||||||
|
- 取证:`workspaces` 表仅 7 行且全是 `C:\Users\maidou\WorkBuddy\<时间戳>`,**不含**任何 `D:\AI技能\*` ⇒ 分组依据不是这张表,而是按会话 cwd 对已打开工作区的聚合。
|
||||||
|
- 修法(配置化,⛔ 不写死):`load-config.js` 增 `sessionCwd`(默认 `''` = 用 ROOT)→ `server.js` `SESSION_CWD = String(CFG.sessionCwd||'').trim() || ROOT` → `config.json` 设 `"sessionCwd": "D:\\AI技能\\mcn-workshop"`。**换分组只改配置,不动代码。**
|
||||||
|
- 实测:新会话 cwd = `D:/AI技能/mcn-workshop` ✅;任务在该工作区执行正常(done,结果"成功",73 秒)。对照 12:23 那条仍在仓库内路径 ⇒ 两组并存,旧的会随过期清理消失。
|
||||||
|
- ✅ 13:06 **二次模拟请求复验一致**(用户指示:不测真实功能,只发模拟请求看分组):写入 `cwds=["D:\\AI技能\\mcn-workshop"]`,会话 cwd 同样命中 ⇒ 配置化改法稳定。两次自检排期均已删,会话留在目标工作区供用户肉眼确认。
|
||||||
|
|
||||||
|
## 素材库补采:批 65 收尾 + 批 66/67 完成(30 卡)
|
||||||
|
|
||||||
|
- 承 09-29 进度:批 65(细节专业 15 卡)已 apply 完成,细节专业 40→55。
|
||||||
|
- **批 66(细节专业 15 卡)**:取材 31748 姜乘澜底妆 / 17511 笑笑易 / 19014 俊希做菜 / 24821 烟道逃生 / 25090 姜乘澜画眼线。✅ **本批 5 条原生即教程/实操题材**,与细节专业天然契合(不像批 65 从非专业题材硬提)。apply:接受 15/退回 0,问法改写 15 张平均 +32 字。细节专业 55→70。
|
||||||
|
- **批 67(自嘲反差 15 卡)**:取材 26484 路之坑爹感恩宴 / 31816 直男五合一洗护 / 31970 李普不离谱 / 21500 桃气小周买饭 / 26411 老弟受难日记。含 1 张**元层面自黑**卡(31970 全家崩溃时推销泡面)。apply:接受 15/退回 0,问法改写 15 张平均 +12 字。自嘲反差 56→71。
|
||||||
|
- 两批均走完整链路:`写 md → _tmp_bNN_spec.py 生成 spec → meta.json → build-pending → cat PROMPT_polish_v3.md + build-pending 组装 prompt → 派 doubao-seed-2-1-pro → _wNN.py 校验落盘 → apply-pending`。
|
||||||
|
|
||||||
|
### 本轮新踩坑
|
||||||
|
- ⚠️ **MCP `myai-mcp-production` 登录态会过期**:批 66 拉第 4 条时报「认证失败」。解法:先 `auth_status` 确认 → `feishu_login` 重授权(拉起浏览器)→ 重试即通。**连续拉多条详情时中途要留意**。
|
||||||
|
- ⚠️ **spec 脚本里别夹英文单词**:批 66 的 neg 句误写「等摊主 processing」,批量产出会污染语料。写完 spec 脚本前先自查。
|
||||||
|
|
||||||
|
### 当前缺口(修正后口径,2026-10-08 10:50)
|
||||||
|
| 标签 | 现有 | 缺口 | 候选 |
|
||||||
|
|:--|--:|--:|--:|
|
||||||
|
| 食材极致 | 8 | 92 | ⛔ 0 |
|
||||||
|
| 预见式服务 | 21 | 79 | ⛔ 2 |
|
||||||
|
| 感官沉浸 | 59 | 41 | 14 |
|
||||||
|
| 细节专业 | 70 | 30 | 22 |
|
||||||
|
| 自嘲反差 | 71 | 29 | 34 |
|
||||||
|
| 视觉冲击 | 76 | 24 | 10 |
|
||||||
|
|
||||||
|
达标:品质对比 110 / 氛围沉浸 106 / 反差 217 / 反常识 128 / 价值观冲击 108。
|
||||||
|
- ⛔ **卡点未解**:`食材极致`(缺口 92)与 `预见式服务`(缺口 79)候选池近乎空,占剩余缺口一半以上。需换关键词或换捞法才能推进。
|
||||||
|
- 暂存批累计 24 批(43–52、54–67,缺 53)。
|
||||||
|
|
||||||
|
## 补采续跑:批 68(感官沉浸 15 卡)/批 69(细节专业 9 卡)
|
||||||
|
|
||||||
|
**批 68 · 感官沉浸(33289 子杭自驾318 ×6 / 31773 萝卜乔乔英国留学回国 ×6 / 30090 姐弟双向送礼 ×3)**
|
||||||
|
- apply 结果:**接受 15/退回 0,问法改写 15 张,平均 +19 字**。感官沉浸 59 → 74。
|
||||||
|
- ⭐ **两条来源 0 卡,已在 md 写明判据**:`28012`(电焊工试戏)男主嘴配的「滋滋」是**谐音梗的音效**而非真实声音质感,事件主角是"身在曹营心在焊" → 归 `反常识`/`自嘲反差`;`28415`(男朋友的算计)踩碎眼镜/摔筷子/拍桌的动静是**发疯甩锅的附产品** → 归 `反差`/`价值观冲击`。
|
||||||
|
- 感官通道分布齐全(冷/缺氧失眠/冰雹砸车/撕羊腿/热水澡/久坐腰酸 · 热瓶/苦到脸垮/中药味/红米肠/赶机喘/长途疲惫 · 中暑虚脱/猛灌水/红糖蛋滋啦)。
|
||||||
|
|
||||||
|
**批 69 · 细节专业(28328 海鲜蒸汽 ×4 / 37316 巧克力棒手作 ×5)= 9 卡**
|
||||||
|
- apply 结果:**接受 9/退回 0,问法改写 9 张,平均 +20 字**。细节专业 70 → 79。
|
||||||
|
- ⭐ **本批 3 条来源 0 卡,坚持不凑数**(沿用批 56 口径):
|
||||||
|
- `20461`(机票骗局)—— 骗子的"专业"是**话术与骗术设计**(伪装客服、真退票取信、连环索要卡号),不落在"这一下换普通人来做就做不到位"的操作细节上 → `反差`/`反常识`。**这是细节专业最容易误判的一类:话术专业 ≠ 操作专业。**
|
||||||
|
- `24820`(烟道脱困)—— 主角是**作死翻车与体力消耗**,片尾"专业人士、专业场地、专业操作"是**反讽** → `自嘲反差`/`难度极限`;且同账号同题材 `24821` 已在批 66 取过两卡,本条脱困手法同构,不重复入库。
|
||||||
|
- `22163`(轮椅游渔岛)—— 倒拖轮椅过软沙滩属**体力付出** → `难度极限`;其余为情侣互坑 → `反差`/`自嘲反差`。
|
||||||
|
- ⭐ **取批命中率下降的信号**:细节专业候选池看着有 22 条,但主队列取 5 条只有 2 条可用(命中率 40%)。后续该标签每批卡数会低于 15,属正常,不要为了凑 15 张硬塞。
|
||||||
|
|
||||||
|
**新增踩坑与复用做法**
|
||||||
|
- ✅ **`_wNN.py` 用 sed 复用**(`sed -e 's/_raw68/_raw69/g' -e 's/pending_0068/pending_0069/g' -e 's/^N = 15/N = 9/' _w68.py > _w69.py`)—— 卡数变化时记得同步改 `N`。本轮连续 3 批(67/68/69)一次通过,`bad: []`。
|
||||||
|
- ✅ **超长详情自动落盘**:`33289` 返回 88,150 字符超限 → 工具落盘到 `D:\.workbuddy\projects\d-AI技能-mcn-short-video\<sessionId>\tool-results\*.txt`,用 Read 直接读(该文件只有 117 行,一次读完)。
|
||||||
|
- ⚠️ 派模型改用「让 Agent 自己 Read prompt 文件 → 自己 Write 落盘 `_rawNN.txt`」——比让模型把 JSON 回吐到对话再手工落盘更省上下文,连续 3 批稳定。
|
||||||
|
|
||||||
|
**当前缺口(批 69 后)**:自嘲反差 29(候选 34)|感官沉浸 26(候选 14)|视觉冲击 24(候选 10)|细节专业 21(候选 22)。食材极致 92/预见式服务 79 仍挂起(用户 2026-10-08 决策:先不处理)。
|
||||||
|
|
||||||
|
## 批 70(自嘲反差 15 卡)· 本轮第 5 批 → 复核点
|
||||||
|
|
||||||
|
**结果**:接受 15/退回 0,问法改写 15 张,平均 **+27 字**(前几批 +19/+20,本批问法增厚更充分)。自嘲反差 71→**86**(缺口 14)。
|
||||||
|
**来源**:35376 唐轩乌龙×6 | 35301 丁浩普信男×6 | 28146 妈妈硬核推理×3。**5 条视频里 2 条 0 卡**(33593、21923)。
|
||||||
|
|
||||||
|
**⭐ 自嘲反差口径新增形态「姿态过载」**(本批确立):
|
||||||
|
- 定义:主角自己把一件小事升格到远超其体量的规格,且全程一本正经(妈妈把女儿回家晚做成刑侦审讯:北风4级/100米/11分37秒 vs 6点12分39秒)。
|
||||||
|
- 判据仍落回一句话:**笑点落在主角自己身上**。妈妈是主角、推理的荒谬感由她自己一本正经地制造 → 收。
|
||||||
|
- ⛔ 反向凡尔赛揭晓(语数英 100/98/100 + 兑现平板)→ 笑点在预期被颠覆 → `反常识`;儿子 O 型嘴石化 → 笑点落在旁观者受击 → `反差`(世界参差)。均不收。
|
||||||
|
|
||||||
|
**两条 0 卡判据(写入 meta)**:
|
||||||
|
- **33593(张开父子的下场)**:家族护短复仇爽剧,爽点落在反派父子被制裁上,主角阵营全程降维打击,无一处笑点落在自己身上 → `反差`/`价值观冲击`。其「悲痛段落硬塞洗面奶口播」虽属元层面自黑,但批 67 已收同构卡(C1 全家崩溃时推销泡面)→ 不重复入库。
|
||||||
|
- **21923(消防设施瘫痪的代价)**:真实事件改编的沉重社会悲剧(烟花引火→消防栓被锁→接头拧不上→母亲丧生→儿子一夜白头),全片无笑点 → `价值观冲击`。
|
||||||
|
- ⭐ **通用判据沉淀**:**沉重社会悲剧类一律不进自嘲反差**;**元层面自黑要查是否已收同构卡**(批 67 已占一类)。
|
||||||
|
|
||||||
|
**其他弃卡判据**:35376「你那算盘珠子都崩我脸上了」是吐槽他人 → `反差`;「我没吃过我妈包的包子」与「我妈不会做饭」是同一场乌龙的两端 → 分作「立 flag」与「揭盅」两卡,不合并(同批 67 B2/B3/B4 细分做法)。35301 防晒植入段属商业植入;丽丽怒骂并入 B4 不单列。
|
||||||
|
|
||||||
|
**本轮 5 批(66–70)合计 69 卡**:66=15 | 67=15(自嘲反差)| 68=15(感官沉浸)| 69=9(细节专业,不凑数)| 70=15(自嘲反差)。
|
||||||
|
**当前缺口(批 70 后)**:感官沉浸 **26**(候选 14)→ 视觉冲击 24(10)→ 细节专业 21(22)→ 自嘲反差 14(34)。食材极致 92/预见式服务 79 挂起。
|
||||||
|
**体检**:`5_tag_coverage.py` 卡片总数 1017(源文件 69 个),12 维无自造标签;⛔ 零覆盖仅 `难度极限`(用户决策不处理),⚠️ 偏少仅 `食材极致`(8,挂起)。
|
||||||
|
|
||||||
|
**复核抽查**:批 70 卡 6(哭着嚼坚果上供)润色生效——台词前置 + 破折号拍点(「我以后再也不养小仓鼠了,我对不起你」——唐轩把相框平放在桌上…),锁定 4 字段零改动,无编造。spec 回写 sim=3/neg=2、tag 单一、src 3 个,全部符合。
|
||||||
|
|
||||||
|
**⭐ 入库状态核查(用户问「素材都同步到 wekonra 了吗」→ 答案:没有,两层原因)**
|
||||||
|
- **流程层**:按暂存模式,补采只落 `batches/pending_import/`,统一入库必须跑 `13_import_pending.py --run`。截至批 70,**27 批 397 卡全部堆在暂存区,一条都没进库**(`13_import_pending.py --help` 盘点:27 批/397 卡/异常 0)。`batches/*.md` 41 批(早期跑过 `2_import_batch.py` 的)是唯一进过库的部分。
|
||||||
|
- **环境层**:`WeKnora-app` 容器 **Exited (127)**,最后一条日志停在 **2026-09-29 23:53**(health 200)→ 31607 端口不通,连"库里现有多少条"都查不了。同项目其余容器(frontend 24719 / docreader / postgres / redis)都在跑。**整个 10-08 的补采从未连过库。**
|
||||||
|
- **部署信息**:镜像 `wechatopenai/weknora-app:latest`,restart `unless-stopped`,compose 工作目录标签 `/home/maidou/weknora`(WSL 路径,本机 `docker ps -a` 可见)。拉起命令 `docker start WeKnora-app`;若仍 127 需查 compose 的 command/entrypoint。
|
||||||
|
- **回炉链路同样未完成**:「删旧条目 + 重导 + 检索终验」都还没做;回炉批 31–33 也未跑完。
|
||||||
|
- ⚠️ 排查口诀补充:**问「同步了吗」时先查两处**——① `13_import_pending.py --help` 看暂存盘点 ② `docker ps -a` 看 WeKnora-app 是否 Up。两者都要看,只查一处会误判。
|
||||||
|
|
||||||
|
**⭐ 规则核对:入库前必须补全 question(用户 2026-10-08 追问「是否遵循」→ 核查 27 批结论)**
|
||||||
|
- **规则依据(能查到的最强出处)**:RUNBOOK §步骤 6「写 spec.json(检索问法)」=流水线必经步骤;**真正的动机在 RUNBOOK 红线**:「追加相似问不重建索引(`POST /faq/entries/{id}/similar-questions` 返回 200 但向量分数按位不变)→ **要改问法只能删 + 重导**」。所以「入库前补全」不是形式要求,而是**事后补的代价=整批删+重导**。
|
||||||
|
- ⚠️ **未找到该规则的原始对话出处**:`conversation_search` 两次 0 命中,本地 memory / RUNBOOK 也没有「入库前要补全 question」这句原话。**已按「规范 + 红线」等价认定,未擅自当作既有明文规则引用。**
|
||||||
|
- **核查结论(27 批全量)**:全部有 spec.json、`std` 全部非空 → **形式上 100% 遵循**。但有 **2 批偏离现行规范**:
|
||||||
|
| 批 | 偏离 | 范围 |
|
||||||
|
|:--|:--|:--|
|
||||||
|
| **43** | `neg=3` 条(现行应为 2),且内容是**关键词短语**(「白酒带货 / 餐厅推荐 / 商务礼仪培训」)不是问句 | 15/15 卡 |
|
||||||
|
| **48** | `sim=2` 条(现行应为 3) | 15/15 卡 |
|
||||||
|
其余 25 批:sim=3(批 47 起)或 sim=6(批 44/45/46,合旧规范)/neg=2,全部合规。
|
||||||
|
- ⚠️ **文档与执行脱节(待用户裁定)**:RUNBOOK §步骤 6 明文写 **sim「口语问法 4–6 条」**,但**批 47 起实际统一为 3 条**(`_wNN.py` 校验也写死 `!= 3`)。要么改文档、要么把 sim 补回 ≥4 条。
|
||||||
|
- ⭐ **核查脚本口诀**:`glob('batches/pending_import/batch_*.spec.json')` 逐批 `Counter(len(x['sim']))` + `Counter(len(x['neg']))` 打分布表,一眼看出条数偏离;**别用 md 的 ```text 计数 //2 算卡数**(会算成一半,以 spec 长度为准)。
|
||||||
|
|
||||||
|
**⭐ Docker WeKnora 启动失败排查(2026-10-08 · 已修复)**
|
||||||
|
- **根因(不是应用崩溃,是容器 init 前的 bind mount 失败)**:`docker inspect WeKnora-app` 的 `.State.Error` 给出原文——
|
||||||
|
`OCI runtime create failed: runc create failed: ... error mounting "/run/desktop/mnt/host/wsl/docker-desktop-bind-mounts/Ubuntu-24.04/4f4eee8c…" to rootfs at "/app/config/config.yaml": not a directory: Are you trying to mount a directory onto a file (or vice-versa)?`
|
||||||
|
- **链路**:compose 写 `./config/config.yaml:/app/config/config.yaml`(文件→文件,写法正确,源文件 `/home/maidou/weknora/config/config.yaml` 确实存在且是 6545 B 的文件)→ 但 **Docker Desktop 的 bind-mount 代理层**(WSL 9P 中转,路径被哈希成 `4f4eee8c…`)把源**识别成了目录** → 挂载失败 → 容器连 init 都没进 → **Exit 127**(entrypoint `./scripts/docker-entrypoint.sh` 根本没执行)。
|
||||||
|
- **时间线**:StartedAt 09-28 01:45,FinishedAt **10-08 02:51** —— 正常跑了 10 天,今天凌晨有一次重启尝试(Docker Desktop/WSL 层变动)时代理层状态不对而失败。restart `unless-stopped` 不会救「启动失败」,所以一直停在 Exited。
|
||||||
|
- ✅ **修复:`docker start WeKnora-app` 一次即恢复**(`Up (healthy)`,31607 通)→ 属**代理层临时状态问题**,重试可解。复发时的升级路径:重启 Docker Desktop → `docker compose up -d` 重建容器(会换新的代理哈希)。治本方向:单文件 bind mount 跨 WSL 本就脆弱,可改 `docker cp` 或 volume。
|
||||||
|
- ⭐ **排查口诀:容器 Exited(127) 先看 `docker inspect --format '{{.State.Error}}'`**,不要只看 `docker logs`(日志只到上次正常运行,看不出启动失败原因)。本次日志最后一条停在 09-29 23:53 health 200,完全看不出问题,是 inspect 的 State.Error 直接给出根因。
|
||||||
|
|
||||||
|
**库内实测(服务恢复后)**
|
||||||
|
- FAQ 总数 **631**(分页抖动,去重拉取 624)。**问法 100% 齐全**:无 std 0 / 无 sim 0 / 无 neg 0。
|
||||||
|
- sim 条数分布 `{5:312, 6:222, 3:46, 2:30, 4:14}`、neg `{2:542, 3:82}` —— 历史口径混着(旧 5/6 条、现 3 条、少量 2 条)。
|
||||||
|
- 库内 tag_name 分布:反差 172/反常识 117/价值观冲击 93/视觉冲击 67/自嘲反差 44/细节专业 32/感官沉浸 26/氛围沉浸 24/品质对比 19/预见式服务 19/食材极致 9/难度极限 2 → **与本地统计一致,即已入库 = 早期 41 批;暂存 27 批 397 卡确实未进。**
|
||||||
|
- ⚠️ **接口结构坑**:FAQ 列表返回是 **`data.data`**(`{data:{total,page,page_size,data:[...]}}`)两层嵌套,只取一层会得到空列表;条目标签字段是 **`tag_name`**(不是 `tag`/`tags`);`page_size=total` 一次拉全会返回空 → 必须分页 100 逐页 + 按 id 去重。
|
||||||
|
|
||||||
|
**新增踩坑**
|
||||||
|
- ⚠️ `_tmp_bNN_spec.py` 里卡片名提取必须 **`.replace("###","")`**:`head.split("|")[0].strip()` 会带 `### ` 前缀 → `assert name in Q` 报 `AssertionError: ### 吃包子吃出丧母感`。批 70 踩到一次,加 replace 后通过。
|
||||||
|
- ✅ prompt 组装字节数核对:模板 7,683 + 载荷 34,647 = 42,330(`wc -c` 可验),比批 68 记录的 15,772 大是因当时统计口径不同,**以 `wc -c` 为准**。
|
||||||
+40
-41
@@ -1,28 +1,40 @@
|
|||||||
# MCN 短视频项目长期记忆(2026-09-28 精简重写 v3)
|
# MCN 短视频项目长期记忆(v4 · 2026-10-08 去重精简)
|
||||||
|
|
||||||
> 原则:**只放「索引 + 红线 + 踩坑」**。凡已在文件里可查的(SKILL.md / references / RUNBOOK / 工作台规范)只留指针。
|
> 原则:**只放「索引 + 红线 + 踩坑」**。凡文件里可查的(SKILL.md / references / RUNBOOK / 工作台 docs)只留指针,不抄内容。历史流水已清。
|
||||||
|
|
||||||
## 0. 权威源指针
|
## 0. 权威源指针
|
||||||
| 主题 | 权威文件 |
|
| 主题 | 权威文件 |
|
||||||
|:--|:--|
|
|:--|:--|
|
||||||
| 技能全部口径 | `<仓库>/project/短视频脚本创作/V1.0/`(SKILL.md + references/references-add) |
|
| 技能全部口径 | `<仓库>/project/短视频脚本创作/V1.0/`(SKILL.md + references + references-add) |
|
||||||
| 路径/环境 | `references-add/路径配置.md` |
|
| 路径 / 环境 | `references-add/路径配置.md` |
|
||||||
| 留人密度 | `知识库/03_框架节奏/04_节奏控时叙事套路.md` §1.3 三级留人点体系 |
|
| 留人密度 | `知识库/03_框架节奏/04_节奏控时叙事套路.md` §1.3 三级留人点体系 |
|
||||||
| 12 维标签 / 镜头 / 画面风格 | `素材库/分块定义/00_通用维度_分块定义.md` |
|
| 12 维标签 / 镜头 / 画面风格 | `素材库/分块定义/00_通用维度_分块定义.md` |
|
||||||
| 事件卡 10 字段模板 | `素材库/分块定义/00_模板-各库分块模板.md` §极致事件 |
|
| 事件卡 10 字段模板 | `素材库/分块定义/00_模板-各库分块模板.md` §极致事件 |
|
||||||
| 赛道词表 | `知识库/标签库/赛道标签.md`(S01–S27 × 8 叙事形态 × 三级垂类) |
|
| 赛道词表 | `知识库/标签库/赛道标签.md`(S01–S27 × 8 叙事形态 × 三级垂类) |
|
||||||
| 卡片批量生产 / 回炉 | `tools/weknora-ingest/RUNBOOK.md`(自包含手册,**§2.4 召回实测 / §2.5 回炉 SOP / §2.6 触发率**) |
|
| 卡片批量生产 / 回炉 | `tools/weknora-ingest/RUNBOOK.md`(§2.4 召回 / §2.5 回炉 SOP / §2.6 触发率) |
|
||||||
| 工作台 UI | `mcn-work-shop/docs/工作台视觉交互规范.md` |
|
| 工作台 UI | `mcn-workshop/docs/工作台视觉交互规范.md` |
|
||||||
|
| 工作台 AI 通路 | `mcn-workshop/docs/AI链路-本地CLI接入.md` |
|
||||||
|
| 技能侧 WeKnora 调用 | 技能 `references/接口调用/WeKnora_极致事件检索.md` |
|
||||||
|
|
||||||
## 1. 仓库与环境
|
## 1. 仓库与环境
|
||||||
- 本地 `D:\AI技能\mcn-short-video`;远程 `[email protected]:maogeigei/mcn-short-video.git`。
|
- 本地 `D:\AI技能\mcn-short-video`;远程 `[email protected]:maogeigei/mcn-short-video.git`。
|
||||||
- 技能软链 `~/.workbuddy/skills/短视频工作台` = junction → `project\短视频脚本创作\V1.0\`;失效判据 children=0 → node `unlinkSync` + PowerShell `New-Item -ItemType Junction`。
|
- 技能软链 `~/.workbuddy/skills/短视频工作台` = junction → `project\短视频脚本创作\V1.0\`;失效判据 children=0 → node `unlinkSync` + PowerShell `New-Item -ItemType Junction`。
|
||||||
- 环境判定(禁写死盘符):路径含 `.dsh` → dsh(**只读**);否则 `git rev-parse --show-toplevel` 成功=开发机 / 失败=用户环境。
|
- 环境判定(禁写死盘符):路径含 `.dsh` → dsh(**只读**);否则 `git rev-parse --show-toplevel` 成功=开发机 / 失败=用户环境。
|
||||||
- 产物落盘:桌面 `MCNSkill项目/{账号}/`。
|
- 产物落盘:桌面 `MCNSkill项目/{账号}/`。
|
||||||
- 技能版本:**只改 V1.0**;Lite1.0(无 subskills)默认不动;变更流程 = 方案→确认→执行→同步副本。
|
- 版本:**只改 V1.0**;Lite1.0(无 subskills)默认不动;变更流程 = 方案→确认→执行→同步副本。
|
||||||
|
|
||||||
## 2. 子技能要点
|
## 2. 子技能要点
|
||||||
- **MCN工作台(Web)** `V1.0/mcn-work-shop/`:零依赖 Node,端口 8900。AI 任务 `POST /api/run`(写宿主 automation,once + `next_run_at`=now+5s)→ 轮询 `/api/run/status`;**创作/改写 prompt 必带 `SKILL_HINT_CREATE` 前缀**。配置单源 `config.json`;`.cjs` 无扩展 require 会 MODULE_NOT_FOUND → 配置模块用 `.js`;静态资源根映射,`/public/xxx` 必 404。
|
|
||||||
|
### 2.1 MCN工作台(Web)`V1.0/mcn-workshop/`
|
||||||
|
- 零依赖 Node,端口 8900。任务 `POST /api/run`(写宿主 automation,once + `now+5s`)→ 轮询 `/api/run/status`;**创作/改写 prompt 必带 `SKILL_HINT_CREATE` 前缀**。配置单源 `config.json`;`.cjs` 无扩展 require → MODULE_NOT_FOUND(配置模块用 `.js`);静态资源根映射,`/public/xxx` 必 404。
|
||||||
|
- **AI 通路(09-29 起)**:`ai.backend` = `cli`(本地 CodeBuddy CLI,stdio 无端口)|`dify`(旧链路兜底)。**细节全在 §0 权威文档,改动前必读**。四条硬红线:
|
||||||
|
- 🔴 **凭据**:`config.json` 是 git 跟踪文件 → key 只放 `%USERPROFILE%\.workbuddy\mcn-workshop\ai-cli.json`(仓库外,不被技能同步复制)。
|
||||||
|
- 🔴 **`SERVER__PORT` 必须删** —— CLI(含 `-p` 模式)内部起 HTTP server,继承宿主端口即 `EADDRINUSE` **静默卡死**,且 HTTP 层(health/info/sessions)看起来完全正常。**最容易误判的坑**。
|
||||||
|
- 🔴 **两段流程模型来源不同**:`/api/ai/clarify`(需求打磨)走**本地 CLI**(`ai.cli.model`);`/api/run`(提交创作)走**宿主调度器**,模型取 `sessions` 表最近活跃**手动会话**的 `model`(兜底 `auto`)⇒ **改 `ai.cli.model` 不影响创作任务模型**。
|
||||||
|
- 🔴 **`custom:` 模型按租户开通**:本机 Key 租户**无 doubao**(500 / `service info not found`);`custom-model-*` 真名从 `%TEMP%\mcn-cli-home\logs\**\*.log` 的 `requestModelId`/`requestModelName` 配对提取(问模型「你是谁」不可靠)。
|
||||||
|
- 其余备忘:性能三杠杆 `--tools "WebSearch,WebFetch"`(prompt -78%)/ `--effort low`(首字 61.7s→19.7s)/ 换模型;⚠️ `[INIT tools]` 是**注册表全量**,判生效看 `usage.input_tokens`;`/api/ai/topics` 是**死代码**;SSE 协议 `{delta}/{replace}/{error}/[DONE]`,cli 与 dify 一致(前端零改动)。
|
||||||
|
|
||||||
|
### 2.2 其他子技能
|
||||||
- **mcn-dou-analysis**:达人表 15 列、TOP6、≥60 条停、29 赛道枚举、浏览器 9223 + 标签页复用、**身份标识禁从 DB 回填**;产物 `POST /api/import/account` 幂等写 `mcn-plugin.db`。
|
- **mcn-dou-analysis**:达人表 15 列、TOP6、≥60 条停、29 赛道枚举、浏览器 9223 + 标签页复用、**身份标识禁从 DB 回填**;产物 `POST /api/import/account` 幂等写 `mcn-plugin.db`。
|
||||||
- **mcn-data-insight**:11 个红狐子技能,榜单即查即缓存。红狐周期 code=3203 → 账号周榜最新可用期恒为**上一周**(非 bug)。
|
- **mcn-data-insight**:11 个红狐子技能,榜单即查即缓存。红狐周期 code=3203 → 账号周榜最新可用期恒为**上一周**(非 bug)。
|
||||||
- **mcn-video-prompt**:F1 反推图/F2 反推视频/F3 生图/F4 分镜;格式优先级 = 用户格式 > 默认 6 层;llm-wiki 必须下沉读原始条目。
|
- **mcn-video-prompt**:F1 反推图/F2 反推视频/F3 生图/F4 分镜;格式优先级 = 用户格式 > 默认 6 层;llm-wiki 必须下沉读原始条目。
|
||||||
@@ -31,12 +43,11 @@
|
|||||||
## 3. 创作红线
|
## 3. 创作红线
|
||||||
- 三态:⚡自动(静默只落最终物)/🤝共创(+确认门)/🛠开发(+全落盘);产物链 05→06→07 禁跳步;硬编码禁止(P0)。
|
- 三态:⚡自动(静默只落最终物)/🤝共创(+确认门)/🛠开发(+全落盘);产物链 05→06→07 禁跳步;硬编码禁止(P0)。
|
||||||
- 脚本必须走 S1-S11,禁会话内直写;开场钩子 3-7s,硬上限 10s。
|
- 脚本必须走 S1-S11,禁会话内直写;开场钩子 3-7s,硬上限 10s。
|
||||||
- **留人点**:L1 极致触点 15-20s / L2 留人点 **每 10 秒 ≥1** / L3 增量点 3-5s。判据 = 观众产生明确「为什么」(抛问题/打破预期/说到自己/给承诺,≥1 种);纯陈述/纯动作/重复前文不算。自检 = ⌈时长÷10⌉。**禁止混层**。类型名沿用五类机制,**禁自造枚举**。
|
- **留人点**:L1 极致触点 15-20s / L2 留人点 **每 10 秒 ≥1** / L3 增量点 3-5s。判据 = 观众产生明确「为什么」(抛问题/打破预期/说到自己/给承诺,≥1 种);纯陈述/纯动作/重复前文不算。自检 = ⌈时长÷10⌉。**禁止混层**,类型名沿用五类机制,**禁自造枚举**。
|
||||||
- **三层定位**:素材库=存素材/极致事件=素材的内容层属性/知识库=讲方法。**事件卡模板与条目落在素材库侧**,`知识库/05_极致事件/` 只放类型层与判据。
|
- **三层定位**:素材库=存素材/极致事件=素材的内容层属性/知识库=讲方法。**事件卡模板与条目落在素材库侧**,`知识库/05_极致事件/` 只放类型层与判据。
|
||||||
- **事件卡 10 字段(顺序固定)**:`极致内容 → 内容含义 → 极致触点 → 事件标签 → 极致类型 → 创作手法 → 镜头语言 → 画面风格 → 复用场景 → 适用场景`。卡片头 `> 所属库:{NN 库名 | 横切未定}` 不占字段位。
|
- **事件卡 10 字段(顺序固定)**:`极致内容 → 内容含义 → 极致触点 → 事件标签 → 极致类型 → 创作手法 → 镜头语言 → 画面风格 → 复用场景 → 适用场景`。卡片头 `> 所属库:{NN 库名 | 横切未定}` 不占字段位。
|
||||||
- 三易混字段:**极致内容**=画面内容+氛围+质感(客观可拍);**内容含义**=`[期待A]—被X打破→[结果B]`;**极致触点**=**效果侧两项:用户心理 / 共鸣**(机制词归标签/类型,效果词不复用标签词)。
|
- 三易混字段:**极致内容**=画面内容+氛围+质感(客观可拍);**内容含义**=`[期待A]—被X打破→[结果B]`;**极致触点**=**效果侧两项:用户心理 / 共鸣**(机制词归标签/类型,效果词不复用标签词)。
|
||||||
- **事件标签 ≥2 值,主标签后缀「(主)」**;「事件标签」≠ 8 库「极致标签」。
|
- **事件标签 ≥2 值,主标签后缀「(主)」**;「事件标签」≠ 8 库「极致标签」。创作手法含**铺垫量级 短≤3s / 中 3-10s / 长>10s**;翻车样本同等入库。**极致触点 ≠ 留人点**。
|
||||||
- 创作手法含**铺垫量级 短≤3s / 中 3-10s / 长>10s**;翻车样本同等入库。**极致触点 ≠ 留人点**。
|
|
||||||
- **复用场景 ≠ 适用场景**:适用场景=原生在哪被调用(从哪来);复用场景=事件结构能搬到哪(到哪去),取值 `{赛道·叙事形态·段落}`,**禁自造赛道名**。
|
- **复用场景 ≠ 适用场景**:适用场景=原生在哪被调用(从哪来);复用场景=事件结构能搬到哪(到哪去),取值 `{赛道·叙事形态·段落}`,**禁自造赛道名**。
|
||||||
- **素材条目形态铁律**:`### {名称}` + `- **字段**:值` 平铺列表;**禁写成多节表格报告**。
|
- **素材条目形态铁律**:`### {名称}` + `- **字段**:值` 平铺列表;**禁写成多节表格报告**。
|
||||||
- 解析逆向:以 `analysis` 场次表为准(±1-2s 偏移);`极限维度` 是**上游自造词,禁直录**,三步归一:剥落差→类型层→落 12 维。
|
- 解析逆向:以 `analysis` 场次表为准(±1-2s 偏移);`极限维度` 是**上游自造词,禁直录**,三步归一:剥落差→类型层→落 12 维。
|
||||||
@@ -44,7 +55,7 @@
|
|||||||
## 4. 检索层:WeKnora + 卡片流水线
|
## 4. 检索层:WeKnora + 卡片流水线
|
||||||
- 容器:`MCN极致事件素材库`(FAQ 型,`e6772e41-7b72-499d-b46c-e5ca7c9d1740`,`127.0.0.1:31607`,key 见 `D:\.workbuddy\mcp.json`)。**执行前必读 RUNBOOK**。
|
- 容器:`MCN极致事件素材库`(FAQ 型,`e6772e41-7b72-499d-b46c-e5ca7c9d1740`,`127.0.0.1:31607`,key 见 `D:\.workbuddy\mcp.json`)。**执行前必读 RUNBOOK**。
|
||||||
- ⛔ **接口四铁律**:① 只走 `POST /faq/entries` 批量通道(单条 `/faq/entry` 建的检索不到);② 改相似问必须 DELETE + 重导;③ 检索必显式 `vector_threshold: 0.5`(默认 0.7);④ dry_run 异步且占通道,须轮询 completed。
|
- ⛔ **接口四铁律**:① 只走 `POST /faq/entries` 批量通道(单条 `/faq/entry` 建的检索不到);② 改相似问必须 DELETE + 重导;③ 检索必显式 `vector_threshold: 0.5`(默认 0.7);④ dry_run 异步且占通道,须轮询 completed。
|
||||||
- ⛔⛔ **检索参数名铁律**:数量参数名 = **`match_count`**;写 `top_k`/`limit`/`size`/`count`/`k` 会**静默忽略并恒返回 10 条**(不报错)。`vector_threshold` **必须显式传**(省略 → 返回 0 条)。阈值只做事后过滤,**不能提升质量**。分数间隔**不可判优**(top1~top10 首尾仅差 0.047)→ 靠**人工复筛落差结构同构性**。
|
- ⛔⛔ **检索参数名铁律**:数量参数 = **`match_count`**;写 `top_k`/`limit`/`size`/`count`/`k` 会**静默忽略并恒返回 10 条**(不报错)。`vector_threshold` **必须显式传**(省略 → 返回 0 条)。阈值只事后过滤,**不能提升质量**;分数间隔**不可判优**(top1~top10 首尾仅差 0.047)→ 靠**人工复筛落差结构同构性**。
|
||||||
- ⛔ **删 FAQ 条目**:`DELETE /knowledge-bases/{KB}/faq/entries` + body `{"ids":[int64,...]}`;`DELETE /faq/entries/{id}` 是 404。
|
- ⛔ **删 FAQ 条目**:`DELETE /knowledge-bases/{KB}/faq/entries` + body `{"ids":[int64,...]}`;`DELETE /faq/entries/{id}` 是 404。
|
||||||
- ⛔ **FAQ 列表分页抖动**:`page_size=100` 页边界会重复。解法 = 先 `page_size=1` 拿 `total` → 再 `page_size={total}` 一次拉全。
|
- ⛔ **FAQ 列表分页抖动**:`page_size=100` 页边界会重复。解法 = 先 `page_size=1` 拿 `total` → 再 `page_size={total}` 一次拉全。
|
||||||
- 其他:FAQ 检索恒为纯向量;条目**单标签**;`CustomMetadata` 不参与过滤;`auto_tag` 必须关。素材卡走 FAQ 型(文档型封顶 9/11)。**规则类文本不进检索层**。
|
- 其他:FAQ 检索恒为纯向量;条目**单标签**;`CustomMetadata` 不参与过滤;`auto_tag` 必须关。素材卡走 FAQ 型(文档型封顶 9/11)。**规则类文本不进检索层**。
|
||||||
@@ -55,36 +66,27 @@
|
|||||||
|
|
||||||
### 4.1 补采自动化与标签治理
|
### 4.1 补采自动化与标签治理
|
||||||
- 自动化 `MCN极致事件素材库·定向补采(每小时3批·暂存模式)`,id `c4d5eae8-e2ff-427a-99aa-d027d354bc19`,`FREQ=HOURLY;INTERVAL=1`,ACTIVE。**rrule 不支持分钟级** → 单次触发连跑 3 批。
|
- 自动化 `MCN极致事件素材库·定向补采(每小时3批·暂存模式)`,id `c4d5eae8-e2ff-427a-99aa-d027d354bc19`,`FREQ=HOURLY;INTERVAL=1`,ACTIVE。**rrule 不支持分钟级** → 单次触发连跑 3 批。
|
||||||
- 脚本:`4_sensory_candidates.py`(感官候选)/`5_tag_coverage.py`(12 标签体检)/`6_target_candidates.py`(定向筛,含 `NEGMAP`)/`7_dispatch_next.py`(**调度器**:算缺口+取批命令+SKIP/PRIORITY)。取批三入口:主队列/`--sensory`/`--target <标签> N`。
|
- 脚本:`4_sensory_candidates`(感官候选)/`5_tag_coverage`(12 标签体检)/`6_target_candidates`(定向筛,含 `NEGMAP`)/`7_dispatch_next`(**调度器**:算缺口+取批命令+SKIP/PRIORITY)。取批三入口:主队列/`--sensory`/`--target <标签> N`。
|
||||||
- **目标「每标签 ≥100 条」**:**严格主角判据** + **食材优先** + **难度极限暂不处理**。缺口跑 `7_dispatch_next.py`,**勿写死**。
|
- **目标「每标签 ≥100 条」**:**严格主角判据** + **食材优先** + **难度极限暂不处理**。缺口跑 `7_dispatch_next.py`,**勿写死**。
|
||||||
- ⭐ **「食材极致」严格主角判据 3 条**:① 部位知识型 ② 验鲜解剖全流程型 ③ 形态重组型。
|
- ⭐ **「食材极致」严格主角判据 3 条**:① 部位知识型 ② 验鲜解剖全流程型 ③ 形态重组型。
|
||||||
- ⭐⭐ **暂存模式(防返工关键机制)**:补采只产 `batches/pending_import/{batch_00NN.md,.spec.json,.meta.json}`,**禁跑 `2_import_batch.py`/`3_done_batch.py`、禁改 `state.json`**。统一入库 = **`13_import_pending.py --dry|--run|--sync`**。**安全结论**:回炉删除按 `polish_plan.json` 冻结的 526 id → 新采卡不会被误删;唯一真风险 = 重跑 `9_polish_plan.py`(已禁)。
|
- ⭐⭐ **暂存模式(防返工关键机制)**:补采只产 `batches/pending_import/{batch_00NN.md,.spec.json,.meta.json}`,**禁跑 `2_import_batch.py`/`3_done_batch.py`、禁改 `state.json`**。统一入库 = **`13_import_pending.py --dry|--run|--sync`**。**安全结论**:回炉删除按 `polish_plan.json` 冻结的 526 id → 新采卡不会被误删;唯一真风险 = 重跑 `9_polish_plan.py`(已禁)。
|
||||||
|
|
||||||
### 4.2 卡片回炉 + v3 双任务 SOP — 详见 RUNBOOK §2.5
|
### 4.2 卡片回炉 + v3 SOP(细节 → RUNBOOK §2.5)
|
||||||
- 链路:`8_backup_all.py` → `9_polish_plan.py` → `10_polish_batch.py payload|apply <n>` → 删旧 + 重导 → `11_diff_report.py`。
|
- 链路 `8_backup_all` → `9_polish_plan` → `10_polish_batch payload|apply <n>` → 删旧 + 重导 → `11_diff_report`。
|
||||||
- **字段契约**:可润色 6(极致内容/内容含义/极致触点/创作手法/镜头语言/画面风格)/**锁定 4**(事件标签/极致类型/复用场景/适用场景)/可改写问法 3 组。
|
- **字段契约**:可润色 6(极致内容/内容含义/极致触点/创作手法/镜头语言/画面风格)/**锁定 4**(事件标签/极致类型/复用场景/适用场景)/可改写问法 3 组。
|
||||||
- **三重校验**:① 字段集合一致 ② 骨架完整 ③ 行数不变。任一失败 → 该卡退回。
|
- **三重校验**:① 字段集合一致 ② 骨架完整 ③ 行数不变。任一失败 → 该卡退回。
|
||||||
- ⭐ **v2「吸引力优先」**:判据 = **内容是否更短视频化**。三特征:破折号 +24%/口语化连接词 +28/强动作动词 +32%。三改动 = **台词前置/破折号拍点/动词升级**。
|
- ⭐ **v2「吸引力优先」**:判据 = **内容是否更短视频化**;三改动 = 台词前置/破折号拍点/动词升级。⭐ **宽约束**(严约束改动率仅 1% / 宽 46%)。
|
||||||
- ⭐ **约束强度对照**:堆「禁止…」严约束 → 改动率仅 1%;宽约束 → 46%。走宽约束。
|
- ⭐ **v3 问法增厚**:`std` 保留骨架补画面、`sim` 条数严格相等、`neg` 保持负例属性。**绝对边界 = 搬运原文已有信息**(每个信息点须能在该卡 6 个可润色字段 + 事件标签找到出处)。
|
||||||
- ⭐ **v3 第二任务:问法增厚**:`std` 保留骨架补画面、`sim` 条数严格相等、`neg` 保持负例属性。**v3.1 绝对边界**:增厚 = **搬运原文已有信息**。自检口诀:「问法里每一个具体信息点,都必须能在该卡 6 个可润色字段 + 事件标签里找到出处;找不到的删掉。」
|
- 🔴 **暂存批 v3 链路**:`build-pending <n>` → 喂 `PROMPT_polish_v3.md` + 载荷 → 存 `out/polish_result/pending_00NN.json` → `apply-pending <n>`;**`apply-pending` 必须同一次执行内跑完**(曾漏跑,续跑须先补,批次 48 实测)。
|
||||||
- ⭐ **`validate()` 返回 5 元组** `(ok, reasons, cleaned, q, autofix)`;**std 前缀校验 = autofix 语义**(原文无前缀时模型误加 → 自动剥离,不退回)。
|
- 其他:`validate()` 返 5 元组 `(ok,reasons,cleaned,q,autofix)`,std 前缀不符走 autofix(剥离、不退回);`fiction_check` 弯/直引号须归一化,只告警不拦截;工具 `11_diff_report`/`12_attractiveness`/`14_form_audit`/`15_sensory_recall`/`16-18*_question_*`。
|
||||||
- ⭐ **`fiction_check` 防误报**:引号字符类匹配弯/直引号;比对须归一化。实测残留告警全为误报 → 只告警不拦截。
|
|
||||||
- ⭐⭐ **暂存批 v3 链路(已跑通)**:`10_polish_batch.py build-pending <n>` → `Agent(model="doubao-seed-2-1-pro")` 喂 `PROMPT_polish_v3.md`+载荷 → 存 `out/polish_result/pending_00NN.json` → `apply-pending <n>`。
|
|
||||||
- ⚠️ **`apply-pending` 必须在同一次执行内跑完**:曾出现「已生成 pending_00NN.json 但未 apply」→ 续跑须先补 apply(批次 48 实测)。
|
|
||||||
- 工具:`12_attractiveness.py`、`11_diff_report.py`、`14_form_audit.py`、`15_sensory_recall.py`(含 `extract_name()`)、`16/17_question_ab.py`、`18/18b_question_validate(.r).py`。
|
|
||||||
|
|
||||||
### 4.3 ⭐⭐ 技能侧 WeKnora 调用(权威源 = 技能 `references/接口调用/WeKnora_极致事件检索.md`)
|
### 4.3 技能侧 WeKnora 调用(权威源 = 技能 `references/接口调用/WeKnora_极致事件检索.md`)
|
||||||
- 🔴 **素材召回触发率 ≈ 0(端到端实测 ×2 路径)**;**口径(用户确立,优先)**:架构 = 「**流程不主动召回素材,知识库按需被取**」= 设计意图,**不改**。`hybrid_search` 0 次 ≠ bug。判据只有两条:① 模型**真能写出桥段** → 跳过 = ✅;② **写不出却仍不检索** = ❌。
|
- 🔴 **素材召回触发率 ≈ 0(端到端实测 ×2 路径)**;**用户口径(优先)**:架构 = 「**流程不主动召回素材,知识库按需被取**」= 设计意图,**不改**。`hybrid_search` 0 次 ≠ bug。判据仅两条:① 模型**真能写出桥段** → 跳过 = ✅;② **写不出却仍不检索** = ❌。
|
||||||
- 实测:4 会话 191 次工具调用、召回 0 次;模型自述「中间步骤已在上下文按序完成,自动模式后台静默」→ 整段脑内跳过。
|
- ⭐ **素材库两个作用位**:① 给事件「填空」(S7 写不出桥段时取机制样本)② 给台词「润色」(S9 取原话/微反应/意外)。**同一链上下游**:S7 触发≈0 → S9 `{material_cards}` 恒空 → ② 退化;**∴ ② 失效是 ① 的直接后果**(修①即自动恢复)。文件位:`7_生成短视频大纲.md` §极致触点素材检索;`9_生成短视频脚本.md` L12 + L101-113。
|
||||||
- **⭐ 素材库两个作用位**:**① 给事件「填空」**(S7 写不出桥段时取机制样本)+ **② 给台词「润色」**(S9 取原话/微反应/意外)。**两者是同一条链上下游**:S7 触发 ≈ 0 → S9 的 `{material_cards}` 恒空 → ② 退化。**∴ ② 失效是 ① 的直接后果**(②修好①自动恢复,改动最小)。
|
- **结论**:不动架构;解「按需判据太软」→ 推荐 **C1 换外部可验证判据**("通用 vs 专属"可验证)。⛔ 作废「删跳过条款 → 默认必检」。
|
||||||
- 文件位置:① `7_生成短视频大纲.md` §极致触点素材检索;② `9_生成短视频脚本.md` L12 + L101-113 §3.1。
|
- ✅ **确定有效**:参数纠正(`match_count` 省 69% 上下文;`vector_threshold` 必显式传)。⚠️ **收益有限**:问法改写(反查法 R@1 34/35→35/35,**差异在噪声内**;⛔ 禁宣称"召回率 97%→100%")。🔴 **真实瓶颈 = 素材覆盖**(3/5 真实节点零命中)。
|
||||||
- **结论**:不动架构;解「"按需"判据太软」→ 推荐 **C1 换外部可验证判据**("通用 vs 专属"可验证,不像"我能不能写")。⛔ 作废「删除跳过条款 → 默认必检」。
|
- 🔬 **验证方法论铁律**:测检索优化 → ⛔ 禁用「凭空需求」法;✅ 必用**「反查法」**。⚠️ 信息保真审计口径 = **角色台词零丢失 + 数字零丢失 + 可拍动作/物品全在**。
|
||||||
- 会话记录 `D:/.workbuddy/projects/<cwd转义名>/<sessionId>.jsonl`;工具 `20_trace_recall.py`。工作台「提交创作」是**两段式**(`#reqCreate` 只弹确认,真提交在 `button[data-ok]`)。
|
- 杂项:会话记录 `D:/.workbuddy/projects/<cwd转义名>/<sessionId>.jsonl`;工具 `20_trace_recall.py`;工作台「提交创作」两段式(`#reqCreate` 弹确认,真提交在 `button[data-ok]`)。
|
||||||
- ✅ **确定有效 = 参数纠正**:`match_count` 实测省 69% 上下文;`vector_threshold` 必显式传。
|
|
||||||
- ⚠️ **收益有限 = 问法改写**:反查法旧 R@1 34/35 vs 新 35/35 → **差异在噪声内**。价值 = **信息量保障**,不是提升召回率。⛔ 禁宣称"召回率 97%→100%"。
|
|
||||||
- 🔴 **真实瓶颈 = 素材覆盖**:需求法暴露 3/5 真实节点零命中 → 问法再优化也召不回不存在的素材。
|
|
||||||
- 🔬 **验证方法论铁律**:测「检索优化」→ ⛔ 禁用「凭空需求」法;✅ 必用**「反查法」**。
|
|
||||||
- ⚠️ **信息保真审计口径**:真判据 = **角色台词零丢失 + 数字零丢失 + 可拍动作/物品全在**。
|
|
||||||
|
|
||||||
## 5. 麦芽平台采集(脚本在 `tools/`)
|
## 5. 麦芽平台采集(脚本在 `tools/`)
|
||||||
- ⭐ **接口直取,禁 DOM 翻页**:`GET /api/scriptwriting/shortVideo/hot?pageNum&pageSize&sortType&startLikeNum`(pageSize=50 最快)。
|
- ⭐ **接口直取,禁 DOM 翻页**:`GET /api/scriptwriting/shortVideo/hot?pageNum&pageSize&sortType&startLikeNum`(pageSize=50 最快)。
|
||||||
@@ -110,14 +112,11 @@
|
|||||||
- ChatGPT 投材料(`tools/chatgpt_ask.py`):**长材料走附件上传**;完成判定 = 无 stop 按钮且文本连续 4 次不变。
|
- ChatGPT 投材料(`tools/chatgpt_ask.py`):**长材料走附件上传**;完成判定 = 无 stop 按钮且文本连续 4 次不变。
|
||||||
|
|
||||||
## 8. 工作台 UI 定稿要点
|
## 8. 工作台 UI 定稿要点
|
||||||
> 完整铁律查 **`mcn-work-shop/docs/工作台视觉交互规范.md`**(权威源)。
|
> 完整铁律查 **`mcn-workshop/docs/工作台视觉交互规范.md`**(权威源)。
|
||||||
- 榜单四榜末列恒「功能」= `inDb ? 查看 : 关注`;热门榜与点赞榜字段体系不同 → 跨榜逻辑双向回退。
|
- 榜单四榜末列恒「功能」= `inDb ? 查看 : 关注`;热门榜与点赞榜字段体系不同 → 跨榜逻辑双向回退。
|
||||||
- 选题列表由 `rewrite_id` 驱动:点标题=详情;「查看脚本」=`openScriptModal({rewriteId})`;「编辑继续」=带 id UPDATE;「复制」=不带 id 新增。
|
- 选题列表由 `rewrite_id` 驱动:点标题=详情;「查看脚本」=`openScriptModal({rewriteId})`;「编辑继续」=带 id UPDATE;「复制」=不带 id 新增。
|
||||||
|
|
||||||
## 9. 历史时间线(压缩)
|
## 9. 历史时间线(压缩)
|
||||||
- 08 月:素材库扁平化 8 类型;赛道 8→27;三态创作;Lite1.0;`.dsh` 只读;工作台立项。
|
- 08 月:素材库扁平化 8 类型;赛道 8→27;三态创作;Lite1.0;`.dsh` 只读;工作台立项。
|
||||||
- 09-10 仓库迁移 + 环境判定|09-14 全量技能审计(P0=0/P1=8/P2=7)|09-16 工作台 AI 创作改版 + UI 规范定稿。
|
- 09-10 仓库迁移 + 环境判定|09-14 全量技能审计(P0=0/P1=8/P2=7)|09-16 工作台 AI 创作改版 + UI 规范定稿|09-21~22 极致事件定义与三层定位裁决、事件卡 10 字段定稿|09-23 WeKnora 检索层建成 + 留人点三级体系定稿|09-23~24 卡片流水线化|09-28 定向补采自动化 + 卡片回炉(v2/v3)+ 补采切暂存模式 + 检索参数名纠正。
|
||||||
- 09-21~22 极致事件定义与三层定位裁决、事件卡 10 字段定稿。
|
- 09-29 AI 通路改 `cli`(本地 CodeBuddy CLI)→ 端到端跑通(status/probe/clarify/run),模型 `deepseek-v4.1-flash`;权威文档 `docs/AI链路-本地CLI接入.md`。
|
||||||
- 09-23 WeKnora 检索层建成(FAQ 容器 + 阈值 0.5);留人点三级体系定稿。
|
|
||||||
- 09-23~24 卡片流水线化(RUNBOOK + 脚本 1/2/3)。
|
|
||||||
- 09-28 定向补采自动化 + 卡片回炉(v2 吸引力优先 + v3 问法增厚)+ 补采切暂存模式;检索参数名纠正。
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -53,7 +53,7 @@
|
|||||||
|-------|-------|---------|-----------|
|
|-------|-------|---------|-----------|
|
||||||
| 项目文件夹(桌面 `MCNSkill项目/俊希/`) | 账号设定/视频表格/脚本 | **仅 `提示词01/`**(分镜产出,属短视频分镜提示词技能) | **否**(本链路产出全新创建;提示词01 保留) |
|
| 项目文件夹(桌面 `MCNSkill项目/俊希/`) | 账号设定/视频表格/脚本 | **仅 `提示词01/`**(分镜产出,属短视频分镜提示词技能) | **否**(本链路产出全新创建;提示词01 保留) |
|
||||||
| 达人账号表.xlsx | 俊希行 | **无** | 否(首次登记 70014) |
|
| 达人账号表.xlsx | 俊希行 | **无** | 否(首次登记 70014) |
|
||||||
| 工作台 mcn-plugin.db(`…\mcn-work-shop\`,自有副本) | hot_accounts 俊希 | **有**(id=3,含完整账号信息 + content 账号设定文本、douyin_id=JJX0827、sec_uid、278.7万粉、source_dir=`D:\dshworkspace\账号解析\俊希` 为**旧路径残留**,目录已不存在) | **否**(见下判定) |
|
| 工作台 mcn-plugin.db(`…\mcn-workshop\`,自有副本) | hot_accounts 俊希 | **有**(id=3,含完整账号信息 + content 账号设定文本、douyin_id=JJX0827、sec_uid、278.7万粉、source_dir=`D:\dshworkspace\账号解析\俊希` 为**旧路径残留**,目录已不存在) | **否**(见下判定) |
|
||||||
| 同上 | account_videos 俊希 | **有 108 条**(source_dir=`D:\dshworkspace\抖音账号分析\俊希` 旧路径) | 否 |
|
| 同上 | account_videos 俊希 | **有 108 条**(source_dir=`D:\dshworkspace\抖音账号分析\俊希` 旧路径) | 否 |
|
||||||
| 同上 | account_persona 俊希 | **无**(5 行均为 1627-1630 测试占位,无 account_id=3) | 否 |
|
| 同上 | account_persona 俊希 | **无**(5 行均为 1627-1630 测试占位,无 account_id=3) | 否 |
|
||||||
| 同上 | rewrite_log 俊希 | **有 3 条**(2026-08-19 AI 写脚本记录,id=12/15/16,topic 含完整选题) | 否 |
|
| 同上 | rewrite_log 俊希 | **有 3 条**(2026-08-19 AI 写脚本记录,id=12/15/16,topic 含完整选题) | 否 |
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
1. **文件层面**:桌面 `MCNSkill项目/俊希/` 无本链路产出 → **无需清除**,全新创建;已有产出时(有就刷新)→ 覆盖更新 `短视频表格.xlsx`、新增/更新设定与脚本,**不删除旧产出**(保留可追溯)
|
1. **文件层面**:桌面 `MCNSkill项目/俊希/` 无本链路产出 → **无需清除**,全新创建;已有产出时(有就刷新)→ 覆盖更新 `短视频表格.xlsx`、新增/更新设定与脚本,**不删除旧产出**(保留可追溯)
|
||||||
2. **数据库层面(工作台 mcn-plugin.db 自有副本)**:俊希有历史数据但**不影响本测试**——关键判定 `account_persona` 无俊希 → 工作台 `confirmAiScript` 的 hasPersona=false → 弹窗走「先进行账号设定分析」分支,**首次场景成立**(hot_accounts.content 的旧设定文本不会被界面读取,界面只读 account_persona)。因此**无需清除**;若追求「完全干净」需用户明确授权后选择性清除(hot_accounts id=3 及其 videos/rewrite_log 行),且只动工作台自有副本
|
2. **数据库层面(工作台 mcn-plugin.db 自有副本)**:俊希有历史数据但**不影响本测试**——关键判定 `account_persona` 无俊希 → 工作台 `confirmAiScript` 的 hasPersona=false → 弹窗走「先进行账号设定分析」分支,**首次场景成立**(hot_accounts.content 的旧设定文本不会被界面读取,界面只读 account_persona)。因此**无需清除**;若追求「完全干净」需用户明确授权后选择性清除(hot_accounts id=3 及其 videos/rewrite_log 行),且只动工作台自有副本
|
||||||
3. **dsh 原库**:**红线只读,绝不写**(清理不适用)
|
3. **dsh 原库**:**红线只读,绝不写**(清理不适用)
|
||||||
4. **工作台副本与 dsh 原库的数据同步**:工作台读自有副本(`mcn-work-shop/mcn-plugin.db`),与 `~/.dsh/mcn-plugin.db` 隔离;测试新增数据(rewrite_log 第 4 条等)只落工作台副本,不影响 dsh 原库
|
4. **工作台副本与 dsh 原库的数据同步**:工作台读自有副本(`mcn-workshop/mcn-plugin.db`),与 `~/.dsh/mcn-plugin.db` 隔离;测试新增数据(rewrite_log 第 4 条等)只落工作台副本,不影响 dsh 原库
|
||||||
|
|
||||||
### 清除操作(如用户要求执行,需先备份)
|
### 清除操作(如用户要求执行,需先备份)
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
|
|
||||||
### TC-09 工作台 AI 参考最新视频选题(**界面模拟点击**)
|
### TC-09 工作台 AI 参考最新视频选题(**界面模拟点击**)
|
||||||
|
|
||||||
> **本环节用可执行脚本模拟真实点击测试**,脚本:`mcn-work-shop/cdp-test-junxi.mjs`
|
> **本环节用可执行脚本模拟真实点击测试**,脚本:`mcn-workshop/cdp-test-junxi.mjs`
|
||||||
> 用法:`node cdp-test-junxi.mjs`(安全模式:验证到选题生成,不触发任务);`node cdp-test-junxi.mjs --confirm`(继续确认执行,真实触发创作任务)
|
> 用法:`node cdp-test-junxi.mjs`(安全模式:验证到选题生成,不触发任务);`node cdp-test-junxi.mjs --confirm`(继续确认执行,真实触发创作任务)
|
||||||
> 机制:连接浏览器 9223 调试实例 → 新开标签页打开 `#/accounts` → `Runtime.evaluate` 模拟点击 → 轮询弹窗/AI 选题 → 自动关闭标签页(不干扰用户页面)。脚本复用 cdp-verify-*.mjs 的 CDP 模式。
|
> 机制:连接浏览器 9223 调试实例 → 新开标签页打开 `#/accounts` → `Runtime.evaluate` 模拟点击 → 轮询弹窗/AI 选题 → 自动关闭标签页(不干扰用户页面)。脚本复用 cdp-verify-*.mjs 的 CDP 模式。
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@
|
|||||||
- `MCNSkill项目/俊希/` 现有仅 `提示词01/` 子目录(分镜产出,属短视频分镜提示词技能),无账号分析产出
|
- `MCNSkill项目/俊希/` 现有仅 `提示词01/` 子目录(分镜产出,属短视频分镜提示词技能),无账号分析产出
|
||||||
- **工作台 mcn-plugin.db 俊希历史数据**:hot_accounts id=3(完整账号信息 + content 设定文本,source_dir=`D:\dshworkspace\账号解析\俊希` **旧路径已不存在**)、account_videos 108 条(source_dir=`D:\dshworkspace\抖音账号分析\俊希` 旧路径)、rewrite_log 3 条(2026-08-19,海鲜大餐主题);**account_persona 无俊希** → hasPersona=false → 弹窗走「先进行账号设定分析」分支,首次场景成立,无需清除
|
- **工作台 mcn-plugin.db 俊希历史数据**:hot_accounts id=3(完整账号信息 + content 设定文本,source_dir=`D:\dshworkspace\账号解析\俊希` **旧路径已不存在**)、account_videos 108 条(source_dir=`D:\dshworkspace\抖音账号分析\俊希` 旧路径)、rewrite_log 3 条(2026-08-19,海鲜大餐主题);**account_persona 无俊希** → hasPersona=false → 弹窗走「先进行账号设定分析」分支,首次场景成立,无需清除
|
||||||
- dsh 原库(~/.dsh/mcn-plugin.db)俊希数据与副本一致(.dsh 只读红线不写)
|
- dsh 原库(~/.dsh/mcn-plugin.db)俊希数据与副本一致(.dsh 只读红线不写)
|
||||||
- **界面模拟点击测试脚本**:`mcn-work-shop/cdp-test-junxi.mjs`(2026-08-31 实测通过:账号列表定位俊希 PASS / AI写脚本弹窗结构 PASS / AI 选题 3 个方向 PASS);注意 `.modal` 选择器会被 index.html 隐藏的 `#settingsModal` 干扰,须限定 `:not([hidden])` 且含 `#aiTopicGen`
|
- **界面模拟点击测试脚本**:`mcn-workshop/cdp-test-junxi.mjs`(2026-08-31 实测通过:账号列表定位俊希 PASS / AI写脚本弹窗结构 PASS / AI 选题 3 个方向 PASS);注意 `.modal` 选择器会被 index.html 隐藏的 `#settingsModal` 干扰,须限定 `:not([hidden])` 且含 `#aiTopicGen`
|
||||||
- MCP 解析单条视频需等待 3-5 分钟;已解析过的视频可直接查询
|
- MCP 解析单条视频需等待 3-5 分钟;已解析过的视频可直接查询
|
||||||
- 工作台 #/reviews「AI脚本诊断」复盘按钮明确按 **mcn-script-review(脚本复盘)技能**执行(本用例主链路不含复盘,如需回归可追加)
|
- 工作台 #/reviews「AI脚本诊断」复盘按钮明确按 **mcn-script-review(脚本复盘)技能**执行(本用例主链路不含复盘,如需回归可追加)
|
||||||
- 产出根目录:开发机 → 桌面 `MCNSkill项目/{达人昵称}/`(`references/路径配置.md` 权威)
|
- 产出根目录:开发机 → 桌面 `MCNSkill项目/{达人昵称}/`(`references/路径配置.md` 权威)
|
||||||
@@ -60,8 +60,8 @@
|
|||||||
### P1-2 | 契约表漏登记 2 个 AI 入口
|
### P1-2 | 契约表漏登记 2 个 AI 入口
|
||||||
|
|
||||||
**现象**
|
**现象**
|
||||||
- `mcn-work-shop/docs/AI会话任务输入输出对照.md`:入口表只有 1–9 行(9 为 clarify,非任务);L127 声明「前缀常量 = CREATE/EXTRACT/STORYBOARD/REVIEW + ACCOUNT」;检查清单第 1 项同样只列 5 个。
|
- `mcn-workshop/docs/AI会话任务输入输出对照.md`:入口表只有 1–9 行(9 为 clarify,非任务);L127 声明「前缀常量 = CREATE/EXTRACT/STORYBOARD/REVIEW + ACCOUNT」;检查清单第 1 项同样只列 5 个。
|
||||||
- 实际 `mcn-work-shop/public/data-pages.js` 还有两个入口:
|
- 实际 `mcn-workshop/public/data-pages.js` 还有两个入口:
|
||||||
|
|
||||||
| 入口 | 触发点 | 前缀常量 | skills | label | 代码位置 |
|
| 入口 | 触发点 | 前缀常量 | skills | label | 代码位置 |
|
||||||
|------|------|------|------|------|------|
|
|------|------|------|------|------|------|
|
||||||
@@ -78,10 +78,10 @@
|
|||||||
| 2 | `对照.md:127` 关键细节 | 常量清单补全为 `SKILL_HINT_CREATE/EXTRACT/STORYBOARD/REVIEW/ANALYZE/VIDEO_PARSE/ACCOUNT`,并注明**权威来源 = data-pages.js / app.js 顶部实际声明**(行号会漂,故不给行号,只给文件) |
|
| 2 | `对照.md:127` 关键细节 | 常量清单补全为 `SKILL_HINT_CREATE/EXTRACT/STORYBOARD/REVIEW/ANALYZE/VIDEO_PARSE/ACCOUNT`,并注明**权威来源 = data-pages.js / app.js 顶部实际声明**(行号会漂,故不给行号,只给文件) |
|
||||||
| 3 | `对照.md:137` 检查清单第 1 项 | 前缀对应关系补 `分析=ANALYZE、视频解析=VIDEO_PARSE` |
|
| 3 | `对照.md:137` 检查清单第 1 项 | 前缀对应关系补 `分析=ANALYZE、视频解析=VIDEO_PARSE` |
|
||||||
| 4 | `对照.md` 新增入口检查清单 | 追加第 8 项:`[ ] 表码对齐:本次新增/改动的入口已回填本表入口表行(含 skills / 前缀常量 / 代码位置),并核对 server.js BROWSER_HINT 正则不被误命中` |
|
| 4 | `对照.md` 新增入口检查清单 | 追加第 8 项:`[ ] 表码对齐:本次新增/改动的入口已回填本表入口表行(含 skills / 前缀常量 / 代码位置),并核对 server.js BROWSER_HINT 正则不被误命中` |
|
||||||
| 5 | `SKILL.md:81` | `(SKILL_HINT_CREATE/EXTRACT/STORYBOARD/REVIEW/ACCOUNT,分别对应 创作/提炼/分镜/复盘/账号抓取)` → `(SKILL_HINT_CREATE/EXTRACT/STORYBOARD/REVIEW/ANALYZE/VIDEO_PARSE/ACCOUNT,分别对应 创作/提炼/分镜/复盘/账号数据分析/视频解析/账号抓取;完整清单以 mcn-work-shop/docs/AI会话任务输入输出对照.md 为准)` |
|
| 5 | `SKILL.md:81` | `(SKILL_HINT_CREATE/EXTRACT/STORYBOARD/REVIEW/ACCOUNT,分别对应 创作/提炼/分镜/复盘/账号抓取)` → `(SKILL_HINT_CREATE/EXTRACT/STORYBOARD/REVIEW/ANALYZE/VIDEO_PARSE/ACCOUNT,分别对应 创作/提炼/分镜/复盘/账号数据分析/视频解析/账号抓取;完整清单以 mcn-workshop/docs/AI会话任务输入输出对照.md 为准)` |
|
||||||
|
|
||||||
**验证**:
|
**验证**:
|
||||||
1. `grep -c "SKILL_HINT_" mcn-work-shop/public/data-pages.js mcn-work-shop/public/app.js` 得到的常量名集合 = 对照表清单,一一对应无缺;
|
1. `grep -c "SKILL_HINT_" mcn-workshop/public/data-pages.js mcn-workshop/public/app.js` 得到的常量名集合 = 对照表清单,一一对应无缺;
|
||||||
2. 逐个入口 grep `skills:` 与表内 skills 列一致。
|
2. 逐个入口 grep `skills:` 与表内 skills 列一致。
|
||||||
|
|
||||||
**风险**:低。纯文档补登记,不改代码逻辑。
|
**风险**:低。纯文档补登记,不改代码逻辑。
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
|
|
||||||
**注意**:`5_生成短视频选题.md:192`「S6 用「本场钩子」五类留人机制」与 S6 场次表列名「本场钩子」是**正确用法**,不动。
|
**注意**:`5_生成短视频选题.md:192`「S6 用「本场钩子」五类留人机制」与 S6 场次表列名「本场钩子」是**正确用法**,不动。
|
||||||
|
|
||||||
**验证**:`grep -rn "开篇钩子\|钩子预设\|钩子策略" SKILL.md references/ mcn-work-shop/` → 仅允许命中 `references-add/变更日志.md`(历史归档)。
|
**验证**:`grep -rn "开篇钩子\|钩子预设\|钩子策略" SKILL.md references/ mcn-workshop/` → 仅允许命中 `references-add/变更日志.md`(历史归档)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
|
|
||||||
| # | 位置 | 动作 |
|
| # | 位置 | 动作 |
|
||||||
|:--:|------|------|
|
|:--:|------|------|
|
||||||
| 1 | `references/接口调用/本地接口调用规范.md` | **重写**为《工作台接口调用规范》:① 基地址 — 从 `mcn-work-shop/config.json` 的 `port` 读(默认 8900,`server.js` 支持 argv 覆盖),禁写死端口;② 写库接口 — `POST /api/import/account`,body `{"account":"{达人昵称}"}`,落库映射见 dou-analysis SKILL.md:160;③ 保留「大文本用 Python urllib + `json.dumps(ensure_ascii=False)` 直连,禁 PowerShell `ConvertTo-Json`」这条铁律;④ 接口清单**不另抄一份**,指向 `mcn-work-shop/docs/AI会话任务输入输出对照.md` + `server.js` 路由为权威 |
|
| 1 | `references/接口调用/本地接口调用规范.md` | **重写**为《工作台接口调用规范》:① 基地址 — 从 `mcn-workshop/config.json` 的 `port` 读(默认 8900,`server.js` 支持 argv 覆盖),禁写死端口;② 写库接口 — `POST /api/import/account`,body `{"account":"{达人昵称}"}`,落库映射见 dou-analysis SKILL.md:160;③ 保留「大文本用 Python urllib + `json.dumps(ensure_ascii=False)` 直连,禁 PowerShell `ConvertTo-Json`」这条铁律;④ 接口清单**不另抄一份**,指向 `mcn-workshop/docs/AI会话任务输入输出对照.md` + `server.js` 路由为权威 |
|
||||||
| 2 | `references/铁律避坑规则/账号设定执行避坑.md:12` | 探测方法改为「用 `GET http://127.0.0.1:<工作台端口>/api/dsh/stats` 探连通,不用写接口探测」 |
|
| 2 | `references/铁律避坑规则/账号设定执行避坑.md:12` | 探测方法改为「用 `GET http://127.0.0.1:<工作台端口>/api/dsh/stats` 探连通,不用写接口探测」 |
|
||||||
| 3 | `references/铁律避坑规则/账号设定执行避坑.md:25` | 沉淀描述改为「新建 `references/接口调用/本地接口调用规范.md`,记录工作台接口基地址判定、写库接口与请求构造方式」 |
|
| 3 | `references/铁律避坑规则/账号设定执行避坑.md:25` | 沉淀描述改为「新建 `references/接口调用/本地接口调用规范.md`,记录工作台接口基地址判定、写库接口与请求构造方式」 |
|
||||||
| 4 | `feature/05_解析视频.md:55` | 整句改为「解析结果对应「原视频解析」数据:落盘后调用工作台 `POST /api/import/account`(body `{"account":"{达人昵称}"}`)把 `content.json` 补录进 `account_video_source` 并提取选题(topic);工作台未启动时跳过(不影响本地保存)。」 |
|
| 4 | `feature/05_解析视频.md:55` | 整句改为「解析结果对应「原视频解析」数据:落盘后调用工作台 `POST /api/import/account`(body `{"account":"{达人昵称}"}`)把 `content.json` 补录进 `account_video_source` 并提取选题(topic);工作台未启动时跳过(不影响本地保存)。」 |
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
### 4. 交互特征小结(对本项目最有参考价值)
|
### 4. 交互特征小结(对本项目最有参考价值)
|
||||||
| 特征 | flova 实现 | 借鉴点 |
|
| 特征 | flova 实现 | 借鉴点 |
|
||||||
|------|-----------|--------|
|
|------|-----------|--------|
|
||||||
| 双栏创作 | 左画布右对话,**对话永驻不打断** | mcn-work-shop 的 AI 弹窗可改为右侧常驻面板 |
|
| 双栏创作 | 左画布右对话,**对话永驻不打断** | mcn-workshop 的 AI 弹窗可改为右侧常驻面板 |
|
||||||
| 创作规范注入 | 对话首轮自动载入 Skill("Skill已完成"状态行) | 用户选定 Skill 后第一步就声明规范,后续全部按规范执行 |
|
| 创作规范注入 | 对话首轮自动载入 Skill("Skill已完成"状态行) | 用户选定 Skill 后第一步就声明规范,后续全部按规范执行 |
|
||||||
| 分步引导 | AI 每次只确认 1 个参数,确认完再问下一个 | 选题→时长→对白→形象→关系→框架→剧本 的渐进式锁定 |
|
| 分步引导 | AI 每次只确认 1 个参数,确认完再问下一个 | 选题→时长→对白→形象→关系→框架→剧本 的渐进式锁定 |
|
||||||
| AI 提供选项 | 回复末尾出 2-4 个**可点选方向**,用户一键续接 | 降低输入成本、保持共创节奏 |
|
| AI 提供选项 | 回复末尾出 2-4 个**可点选方向**,用户一键续接 | 降低输入成本、保持共创节奏 |
|
||||||
|
|||||||
@@ -64,21 +64,21 @@ agent_created: true
|
|||||||
|
|
||||||
用户说「打开工作台 / 打开MCN工作台」时:
|
用户说「打开工作台 / 打开MCN工作台」时:
|
||||||
|
|
||||||
1. 后台启动本地服务:`node <本技能目录>/mcn-work-shop/server.js`(零依赖 Node,无需安装包;Windows 也可双击 `start.bat`)
|
1. 后台启动本地服务:`node <本技能目录>/mcn-workshop/server.js`(零依赖 Node,无需安装包;Windows 也可双击 `start.bat`)
|
||||||
2. 用内置浏览器打开 `http://localhost:8900`。**注意端口**:本机 8899 可能被 `MiPCAudio.exe`(小米 PC 管家)占用系统级 0.0.0.0:8899 且杀后自愈,工作台默认端口已迁移为 **8900**;如需指定端口用 `node server.js <端口>`(`PORT_BASE = parseInt(process.argv[2],10) || 8900`)
|
2. 用内置浏览器打开 `http://localhost:8900`。**注意端口**:本机 8899 可能被 `MiPCAudio.exe`(小米 PC 管家)占用系统级 0.0.0.0:8899 且杀后自愈,工作台默认端口已迁移为 **8900**;如需指定端口用 `node server.js <端口>`(`PORT_BASE = parseInt(process.argv[2],10) || 8900`)
|
||||||
3. 工作台采用 hash 路由,页面结构:
|
3. 工作台采用 hash 路由,页面结构:
|
||||||
- **首页(#/)**:数据功能卡片(抖音周榜/热门账号/AI写脚本/AI写作复盘)+ 5 大技能功能块(问法点击复制)+ 三种创作模式
|
- **首页(#/)**:数据功能卡片(抖音周榜/热门账号/AI写脚本/AI写作复盘)+ 5 大技能功能块(问法点击复制)+ 三种创作模式
|
||||||
- **📈 抖音周榜(#/ranking)**:读桌面 `MCNSkill项目\三方数据\红狐数据\抖音榜单\*.json`(运行时解析真实桌面、兼容 OneDrive 重定向;环境变量 `MCN_RANKING_DIR` 可覆盖),赛道筛选 + 日期切换 + 数值排序
|
- **📈 抖音周榜(#/ranking)**:读桌面 `MCNSkill项目\三方数据\红狐数据\抖音榜单\*.json`(运行时解析真实桌面、兼容 OneDrive 重定向;环境变量 `MCN_RANKING_DIR` 可覆盖),赛道筛选 + 日期切换 + 数值排序
|
||||||
- **👥 热门账号(#/accounts)**:读工作台自有数据库副本 `mcn-work-shop/mcn-plugin.db`(只读),分页/搜索/赛道/类型筛选,账号点击进详情
|
- **👥 热门账号(#/accounts)**:读工作台自有数据库副本 `mcn-workshop/mcn-plugin.db`(只读),分页/搜索/赛道/类型筛选,账号点击进详情
|
||||||
- **账号详情(#/account/:id)**:信息卡 + 账号信息/账号设定/数据分析 3 tab + 视频列表分页;解析/改写/提炼按钮直接提交 AI 任务执行
|
- **账号详情(#/account/:id)**:信息卡 + 账号信息/账号设定/数据分析 3 tab + 视频列表分页;解析/改写/提炼按钮直接提交 AI 任务执行
|
||||||
- **视频详情(#/video/:id)**:信息卡 + 视频脚本/视频拆解/视频分析 3 tab
|
- **视频详情(#/video/:id)**:信息卡 + 视频脚本/视频拆解/视频分析 3 tab
|
||||||
- **✍️ AI写脚本(#/rewrites)**:爆款视频列表 + 源脚本 vs AI脚本双栏对比 + 分镜提示词分块渲染
|
- **✍️ AI写脚本(#/rewrites)**:爆款视频列表 + 源脚本 vs AI脚本双栏对比 + 分镜提示词分块渲染
|
||||||
- **🧪 AI写作复盘(#/reviews)**:评分维度列表 + 复盘详情(评分条/问题清单/报告)
|
- **🧪 AI写作复盘(#/reviews)**:评分维度列表 + 复盘详情(评分条/问题清单/报告)
|
||||||
- **📁 产出浏览(#/files)**:默认读桌面 `MCNSkill项目/`,支持 ⚙️ 设置添加其他项目地址;md/json 在线预览
|
- **📁 产出浏览(#/files)**:默认读桌面 `MCNSkill项目/`,支持 ⚙️ 设置添加其他项目地址;md/json 在线预览
|
||||||
4. **数据边界**:数据库读工作台自有副本 `mcn-work-shop/mcn-plugin.db`(**dsh 侧已整套下线(09-10):`~/.dsh/mcn-plugin.db` 与 `D:\dshworkspace` 均不存在**,工作台 DB 为唯一读写副本);周榜读桌面 `MCNSkill项目\三方数据\红狐数据\抖音榜单\*.json` 文件
|
4. **数据边界**:数据库读工作台自有副本 `mcn-workshop/mcn-plugin.db`(**dsh 侧已整套下线(09-10):`~/.dsh/mcn-plugin.db` 与 `D:\dshworkspace` 均不存在**,工作台 DB 为唯一读写副本);周榜读桌面 `MCNSkill项目\三方数据\红狐数据\抖音榜单\*.json` 文件
|
||||||
5. **AI 任务触发(Automation 方案)**:各数据页「导入/创作/解析/改写/提炼/复盘」按钮点击后 → 工作台后端 `POST /api/run` → 向 `~/.workbuddy/workbuddy.db` 的 `automations` 表写入一条**一次性任务**(`schedule_type='once'`、**`scheduled_at=now+5s`(带秒)且显式写 `next_run_at=now+5000`**、`cwds=[<工作台目录>]`(由 `server.js` 从 `ROOT` 动态推导,值 = `<技能根>/mcn-work-shop`,**禁止写死盘符**))→ WorkBuddy 客户端调度器按 `next_run_at` 扫描拾取(周期≤30s)→ 创建 AI 会话执行 → **会话归入 `mcn-work-shop` 空间分组**(左侧会话栏按 cwd 分组,可在该分组查看任务执行过程与结果)。参考 dsh 插件 `dsh-plugin-mcn` 的 `/mcn/api/creative` 模式(固定会话 followup 触发);前端 5 秒防连点。**调度机制/状态机/并发上限/排查命令详见同目录 `自动化任务调度机制.md`(09-01 实测定稿,勿再按旧认知 `scheduled_at=now` 直写——会因 next_run_at 为空而卡死)**
|
5. **AI 任务触发(Automation 方案)**:各数据页「导入/创作/解析/改写/提炼/复盘」按钮点击后 → 工作台后端 `POST /api/run` → 向 `~/.workbuddy/workbuddy.db` 的 `automations` 表写入一条**一次性任务**(`schedule_type='once'`、**`scheduled_at=now+5s`(带秒)且显式写 `next_run_at=now+5000`**、`cwds=[<工作台目录>]`(由 `server.js` 从 `ROOT` 动态推导,值 = `<技能根>/mcn-workshop`,**禁止写死盘符**))→ WorkBuddy 客户端调度器按 `next_run_at` 扫描拾取(周期≤30s)→ 创建 AI 会话执行 → **会话归入 `mcn-workshop` 空间分组**(左侧会话栏按 cwd 分组,可在该分组查看任务执行过程与结果)。参考 dsh 插件 `dsh-plugin-mcn` 的 `/mcn/api/creative` 模式(固定会话 followup 触发);前端 5 秒防连点。**调度机制/状态机/并发上限/排查命令详见同目录 `自动化任务调度机制.md`(09-01 实测定稿,勿再按旧认知 `scheduled_at=now` 直写——会因 next_run_at 为空而卡死)**
|
||||||
6. **工作台任务执行方式铁律(P0,09-02 固化)**:凡工作台场景的**创作/改写/重写/复盘/解析**类任务(含"重新执行历史任务"),必须通过 `POST /api/run` 提交 AI 会话任务执行(automation 触发 → 独立 AI 会话按技能 S1-S11 执行 → 写库 rewrite_log/入库),**禁止在会话内直接产出脚本草稿**(不会入库、不在左侧会话栏可见、不走技能会话,等于"影子脚本")。**重新执行历史任务 = 复用原任务的 prompt(人设卡+视频选题+创作要求+落库字段)+ `skills:['短视频工作台']` 提交新任务**——新任务自动加载当前技能版本(含最新优化),无需手工搬内容。执行方式判定细则见「关键规则 〇·执行方式判定」。
|
6. **工作台任务执行方式铁律(P0,09-02 固化)**:凡工作台场景的**创作/改写/重写/复盘/解析**类任务(含"重新执行历史任务"),必须通过 `POST /api/run` 提交 AI 会话任务执行(automation 触发 → 独立 AI 会话按技能 S1-S11 执行 → 写库 rewrite_log/入库),**禁止在会话内直接产出脚本草稿**(不会入库、不在左侧会话栏可见、不走技能会话,等于"影子脚本")。**重新执行历史任务 = 复用原任务的 prompt(人设卡+视频选题+创作要求+落库字段)+ `skills:['短视频工作台']` 提交新任务**——新任务自动加载当前技能版本(含最新优化),无需手工搬内容。执行方式判定细则见「关键规则 〇·执行方式判定」。
|
||||||
7. **AI 任务入口技能契约(P0,09-02 固化)**:工作台每个触发 AI 会话任务的入口都必须遵守「功能入口 × 技能调用契约」——任务 prompt = **固定技能声明前缀常量**(SKILL_HINT_CREATE/EXTRACT/STORYBOARD/REVIEW/ACCOUNT,分别对应 创作/提炼/分镜/复盘/账号抓取)+ **业务正文**,并传对应 `skills` 数组(参数级技能挂载)。**新增/修改任何入口时先读 `mcn-work-shop/docs/AI会话任务输入输出对照.md`(契约总表 + 新增入口检查清单 7 项),再动手**——防裸 prompt 跳步(09-02 三入口缺约束根因)、防技能声明混入业务串、防非抓取任务命中 server.js BROWSER_HINT 正则被误注入浏览器锁。
|
7. **AI 任务入口技能契约(P0,09-02 固化)**:工作台每个触发 AI 会话任务的入口都必须遵守「功能入口 × 技能调用契约」——任务 prompt = **固定技能声明前缀常量**(SKILL_HINT_CREATE/EXTRACT/STORYBOARD/REVIEW/ACCOUNT,分别对应 创作/提炼/分镜/复盘/账号抓取)+ **业务正文**,并传对应 `skills` 数组(参数级技能挂载)。**新增/修改任何入口时先读 `mcn-workshop/docs/AI会话任务输入输出对照.md`(契约总表 + 新增入口检查清单 7 项),再动手**——防裸 prompt 跳步(09-02 三入口缺约束根因)、防技能声明混入业务串、防非抓取任务命中 server.js BROWSER_HINT 正则被误注入浏览器锁。
|
||||||
8. 本功能仅 V1.0 源技能环境提供,**不同步 dsh/Lite 副本**
|
8. 本功能仅 V1.0 源技能环境提供,**不同步 dsh/Lite 副本**
|
||||||
9. **改完必重开页面(硬性约定)**:对工作台任何页面/代码改动完成后,**最后一步必须用 present_files 重新打开对应页面**(带 hash 直接定位,如 `http://localhost:8900/#/accounts`),让用户在 WorkBuddy 内置浏览器立即看到最新效果。原因:内置浏览器无 CDP 调试端口(9222/9224 等均无响应),无法模拟 F5;重开页面是唯一可靠刷新方式。此约定对工作台所有 UI 改动强制生效
|
9. **改完必重开页面(硬性约定)**:对工作台任何页面/代码改动完成后,**最后一步必须用 present_files 重新打开对应页面**(带 hash 直接定位,如 `http://localhost:8900/#/accounts`),让用户在 WorkBuddy 内置浏览器立即看到最新效果。原因:内置浏览器无 CDP 调试端口(9222/9224 等均无响应),无法模拟 F5;重开页面是唯一可靠刷新方式。此约定对工作台所有 UI 改动强制生效
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ agent_created: true
|
|||||||
- **技能本体**:`subskills/lieflat-charts/`(相对本文件;含 SKILL.md + catalog.md + templates/ 各 gallery,随源仓库分发)
|
- **技能本体**:`subskills/lieflat-charts/`(相对本文件;含 SKILL.md + catalog.md + templates/ 各 gallery,随源仓库分发)
|
||||||
- **选型规则**:先读 `catalog.md` 按数据形状锁定图型编号,再打开 `templates/` 对应 gallery 的真实实现复制骨架;默认顺序 Lupi Editorial → Lupi Basics → Glance(工作台场景数据量小/需快读时可直用 Glance)
|
- **选型规则**:先读 `catalog.md` 按数据形状锁定图型编号,再打开 `templates/` 对应 gallery 的真实实现复制骨架;默认顺序 Lupi Editorial → Lupi Basics → Glance(工作台场景数据量小/需快读时可直用 Glance)
|
||||||
- **视觉 token**:`mono-tokens.js`(纸 #F0EFEB / 墨 #1C1C1A / 灰 #8F8E88 / 网格 #DEDDD6),同一交付只锁一种色系
|
- **视觉 token**:`mono-tokens.js`(纸 #F0EFEB / 墨 #1C1C1A / 灰 #8F8E88 / 网格 #DEDDD6),同一交付只锁一种色系
|
||||||
- **ECharts 依赖**:已本地化至工作台 `mcn-work-shop/public/vendor/echarts.min.js`(零 CDN 依赖,离线可用);引用样例见工作台账号详情页视频列表上方「点赞/评论趋势折线图」(G8 Rainfall 骨架:双 grid、双序列随时间、tipLight tooltip、quarticOut 入场)
|
- **ECharts 依赖**:已本地化至工作台 `mcn-workshop/public/vendor/echarts.min.js`(零 CDN 依赖,离线可用);引用样例见工作台账号详情页视频列表上方「点赞/评论趋势折线图」(G8 Rainfall 骨架:双 grid、双序列随时间、tipLight tooltip、quarticOut 入场)
|
||||||
- **使用限制**:库外新造是最后手段(须继承最接近 gallery 模板的视觉语法);不得拼接多个模板造混合图型
|
- **使用限制**:库外新造是最后手段(须继承最接近 gallery 模板的视觉语法);不得拼接多个模板造混合图型
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -338,7 +338,7 @@ S1 意图识别 → S2 需求完善 → [G0 分析确认]
|
|||||||
|
|
||||||
> 判定口诀:**"这个产物要不要进工作台(rewrite_log/入库/左侧会话栏)?"**——要进,就走任务;不进,才可会话内产出。
|
> 判定口诀:**"这个产物要不要进工作台(rewrite_log/入库/左侧会话栏)?"**——要进,就走任务;不进,才可会话内产出。
|
||||||
> 重新执行历史任务 = 复用原任务 prompt(从 automations 表取)+ `skills` 参数提交新任务,任务自动应用当前技能版本。
|
> 重新执行历史任务 = 复用原任务 prompt(从 automations 表取)+ `skills` 参数提交新任务,任务自动应用当前技能版本。
|
||||||
> **组装 prompt/选择 skills 时遵守「功能入口 × 技能调用契约」**(工作台章节第 7 条 + `mcn-work-shop/docs/AI会话任务输入输出对照.md`)——任务必须带技能前缀常量(SKILL_HINT_*)+ 对应 `skills` 数组。
|
> **组装 prompt/选择 skills 时遵守「功能入口 × 技能调用契约」**(工作台章节第 7 条 + `mcn-workshop/docs/AI会话任务输入输出对照.md`)——任务必须带技能前缀常量(SKILL_HINT_*)+ 对应 `skills` 数组。
|
||||||
|
|
||||||
### 〇·1、方案与执行分离(P0)
|
### 〇·1、方案与执行分离(P0)
|
||||||
|
|
||||||
@@ -397,7 +397,7 @@ Skill部署到非原始开发路径时,所有用户新增的创作流程/接
|
|||||||
├── SKILL.md ← 入口文件(流程引擎+规则摘要+子技能索引+目录索引)
|
├── SKILL.md ← 入口文件(流程引擎+规则摘要+子技能索引+目录索引)
|
||||||
├── 帮助文档.md ← 使用指南+触发示例+快速启动模板
|
├── 帮助文档.md ← 使用指南+触发示例+快速启动模板
|
||||||
├── scripts/ ← MCP配置 + Dify接口脚本
|
├── scripts/ ← MCP配置 + Dify接口脚本
|
||||||
├── mcn-work-shop/ ← 本地 Web 工作台(server.js + public/,零依赖 Node)
|
├── mcn-workshop/ ← 本地 Web 工作台(server.js + public/,零依赖 Node)
|
||||||
├── subskills/ ← 6 个子技能(browser-harness / lieflat-charts / mcn-data-insight / mcn-dou-analysis / mcn-script-review / mcn-video-prompt,相对路径引用见「子技能索引」)
|
├── subskills/ ← 6 个子技能(browser-harness / lieflat-charts / mcn-data-insight / mcn-dou-analysis / mcn-script-review / mcn-video-prompt,相对路径引用见「子技能索引」)
|
||||||
├── references/ ← 标准库
|
├── references/ ← 标准库
|
||||||
│ ├── 创作流程规范.md ← 确认门/格式/路径/硬编码 规则权威源
|
│ ├── 创作流程规范.md ← 确认门/格式/路径/硬编码 规则权威源
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
# AI 链路:本地 CodeBuddy CLI 接入(A 方案)
|
|
||||||
|
|
||||||
> 2026-09-29 落地。工作台的 AI 能力(需求打磨 / 创作)除 Dify 外,新增**本地 CodeBuddy CLI** 通路。
|
|
||||||
> 实现文件:`cli-backend.js`(后端)+ `server.js` 的 `/api/ai/clarify`、`/api/ai/status`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 一句话
|
|
||||||
|
|
||||||
把「工作台 → 模型」从 **HTTP 调 Dify** 换成 **spawn 一个 `codebuddy` 子进程、用 stdio 通信**。
|
|
||||||
不涉及任何本地端口,与 WorkBuddy 桌面程序完全解耦。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 为什么不是「直接用宿主那个 CLI」
|
|
||||||
|
|
||||||
排查中确认了三条硬结论,决定了必须自备凭据:
|
|
||||||
|
|
||||||
| 结论 | 证据 |
|
|
||||||
|:--|:--|
|
|
||||||
| 宿主 gateway 端口/密码**每轮轮换**,且无处落盘 | 同机同天实测 `12134 → 13116 → 10261`;`D:/.workbuddy/` 下无 worker/registry 文件 |
|
|
||||||
| 唯一发现通道是环境变量,且**只存在于 WorkBuddy 派生的进程树内** | `CODEBUDDY_SERVICE_PROXY_URL=http://127.0.0.1:<port>/...`,用户双击 `start.bat` 起的工作台拿不到 |
|
|
||||||
| WorkBuddy **故意**不让子进程继承凭据 | 用 4 个环境变量把凭据交给 CLI,CLI 读完**立即 `delete`**;凭据不落明文盘 |
|
|
||||||
|
|
||||||
→ 所以走官方文档给的非交互认证通道:`CODEBUDDY_API_KEY` / `CODEBUDDY_AUTH_TOKEN`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 配置步骤
|
|
||||||
|
|
||||||
### 步骤 1:拿 API Key
|
|
||||||
|
|
||||||
| 版本 | 地址 |
|
|
||||||
|:--|:--|
|
|
||||||
| **中国版**(本机是这版,`CODEBUDDY_INTERNET_ENVIRONMENT=internal`) | https://copilot.tencent.com/profile/ |
|
|
||||||
| 国际版 | https://www.codebuddy.ai/profile/keys |
|
|
||||||
| iOA | https://tencent.sso.copilot.tencent.com/profile/keys |
|
|
||||||
|
|
||||||
### 步骤 2:填进 `config.json`
|
|
||||||
|
|
||||||
```json
|
|
||||||
"ai": {
|
|
||||||
"backend": "cli",
|
|
||||||
"cli": {
|
|
||||||
"path": "",
|
|
||||||
"model": "custom:doubao-seed-2-1-pro-260628",
|
|
||||||
"apiKey": "<你的 API Key>",
|
|
||||||
"authToken": "",
|
|
||||||
"internetEnvironment": "internal",
|
|
||||||
"permissionMode": "",
|
|
||||||
"timeoutMs": 300000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
只改两个值即可:`backend` → `"cli"`,`apiKey` → 你的 key。
|
|
||||||
|
|
||||||
### 步骤 3:自检
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 只报告配置(零消耗)
|
|
||||||
curl "http://localhost:8900/api/ai/status"
|
|
||||||
|
|
||||||
# 真跑一次极短提示,验证端到端(消耗一次调用)
|
|
||||||
curl "http://localhost:8900/api/ai/status?probe=1"
|
|
||||||
```
|
|
||||||
|
|
||||||
期望看到 `"ready": true` 且 `probe.ok = true`。若 `ready: false` → key 没填对;
|
|
||||||
若 `probe.error` 里出现 `Authentication required` → key 无效或 `internetEnvironment` 版本错了。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 字段说明
|
|
||||||
|
|
||||||
| 字段 | 说明 |
|
|
||||||
|:--|:--|
|
|
||||||
| `ai.backend` | `"cli"` 走本地 CLI;`"dify"` 走原链路。可用环境变量 `MCN_AI_BACKEND` 临时覆盖(不改文件、不重启) |
|
|
||||||
| `ai.cli.path` | 留空 → 自动探测:`CODEBUDDY_CLI_PATH` 环境变量 → WorkBuddy 自带 CLI → `PATH` 里的 `codebuddy` |
|
|
||||||
| `ai.cli.model` | 模型 ID。可用值见 `codebuddy --model` 帮助(含 `custom:doubao-seed-2-1-pro-260628`) |
|
|
||||||
| `ai.cli.apiKey` | 留空 → 读环境变量 `CODEBUDDY_API_KEY` |
|
|
||||||
| `ai.cli.authToken` | 留空 → 读环境变量 `CODEBUDDY_AUTH_TOKEN`。**优先级最高**(官方:`AUTH_TOKEN` > `apiKeyHelper` > `API_KEY`) |
|
|
||||||
| `ai.cli.internetEnvironment` | 中国版 `internal`;iOA `ioa`;国际版留空 |
|
|
||||||
| `ai.cli.timeoutMs` | 单次调用超时,默认 300000(5 分钟) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 实现里必须保留的三条防护(改动前务必读)
|
|
||||||
|
|
||||||
`cli-backend.js` 的 `buildChildEnv()` 做了三件事,**每一条都是实测踩出来的坑**:
|
|
||||||
|
|
||||||
### 5.1 删掉 `SERVER__PORT` / `SERVER__HOST`
|
|
||||||
|
|
||||||
`codebuddy`(**连 `-p` 打印模式也**)内部会起一个 HTTP server:
|
|
||||||
|
|
||||||
```js
|
|
||||||
listen(parseInt(process.env.SERVER__PORT) || config.get("cell.server", { port: 3000 }).port)
|
|
||||||
```
|
|
||||||
|
|
||||||
在 WorkBuddy 进程树内启动时,会继承宿主给自己设的 `SERVER__PORT` → 去 listen 宿主已占用的端口 →
|
|
||||||
`EADDRINUSE` → **进程静默卡死**(HTTP 层看似正常,只有 agent 执行环节挂掉,极具迷惑性)。
|
|
||||||
|
|
||||||
官方自己在派生子进程时也是这么做的:`delete el.SERVER__PORT, delete el.SERVER__HOST`。
|
|
||||||
|
|
||||||
### 5.2 强制 `CODEBUDDY_FORCE_HEADLESS_BUNDLE=1`
|
|
||||||
|
|
||||||
`bin/codebuddy` 这个 launcher 按环境变量选 bundle:
|
|
||||||
|
|
||||||
| 环境变量 | 命中产物 |
|
|
||||||
|:--|:--|
|
|
||||||
| `CODEBUDDY_FORCE_LITE_WB_BUNDLE=1`(宿主注入,**优先级最高**) | `codebuddy-lite-wb.mjs`(WorkBuddy 特供精简包) |
|
|
||||||
| `CODEBUDDY_FORCE_HEADLESS_BUNDLE=1` | `codebuddy-headless.js`(官方 headless,**我们要的**) |
|
|
||||||
| 含 `--print` | 自动 headless |
|
|
||||||
| 都没有 | `dist/codebuddy`(**本安装里不存在** → `MODULE_NOT_FOUND`) |
|
|
||||||
|
|
||||||
不显式清除 wb 特供包分流,会误用给宿主定制的产物。
|
|
||||||
|
|
||||||
### 5.3 清掉 `NODE_OPTIONS` 与会话上下文变量
|
|
||||||
|
|
||||||
宿主会注入 `NODE_OPTIONS=--require=...node-language-shim.cjs`、`CODEBUDDY_SESSION_*`、
|
|
||||||
`CODEBUDDY_SERVICE_PROXY_URL`、`BAGGAGE` 等,子进程继承会串味。一并清掉,并把
|
|
||||||
`CODEBUDDY_CONFIG_DIR` 指向独立目录(默认 `<tmpdir>/mcn-cli-home`),避免污染 WorkBuddy 的配置与会话历史。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 输出协议
|
|
||||||
|
|
||||||
工作台对前端只暴露一套 SSE 协议,`cli` 与 `dify` 两条后端产出完全一致,**前端无需感知差异**:
|
|
||||||
|
|
||||||
| 事件 | 含义 |
|
|
||||||
|:--|:--|
|
|
||||||
| `{"delta": "..."}` | 文本增量,直接追加渲染 |
|
|
||||||
| `{"replace": "..."}` | 整体覆盖(终态校准 / 出错但已有产出) |
|
|
||||||
| `{"error": "..."}` | 错误(附带可操作的中文提示) |
|
|
||||||
| `data: [DONE]` | 流结束 |
|
|
||||||
|
|
||||||
CLI 侧的事件解析(`--output-format stream-json --include-partial-messages`):
|
|
||||||
|
|
||||||
| CLI 事件 | 处理 |
|
|
||||||
|:--|:--|
|
|
||||||
| `type:"system", subtype:"init"` | 取 `session_id` |
|
|
||||||
| `type:"stream_event"` → `event.content_block_delta` → `delta.type==="text_delta"` | 取 `delta.text` 作为增量 |
|
|
||||||
| `delta.type==="thinking_delta"` | 思考增量(可选回调) |
|
|
||||||
| `type:"assistant"` | 未产生增量流时的整段文本兜底 |
|
|
||||||
| `type:"result"` | 终态。`is_error:true` 时真实原因在 **`errors[]`**,正文作废 |
|
|
||||||
|
|
||||||
> ⚠️ 未认证时,CLI 会把报错文本**当成 assistant 文本**发出来。
|
|
||||||
> `isCliBoilerplate()` 负责识别并拦截,避免把英文报错显示成"AI 的回答"。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 排障速查
|
|
||||||
|
|
||||||
| 现象 | 原因 | 处理 |
|
|
||||||
|:--|:--|:--|
|
|
||||||
| 进程卡死无输出 | `SERVER__PORT` 未清除 → EADDRINUSE | 确认 `buildChildEnv` 的 delete 生效 |
|
|
||||||
| `Cannot find module '../dist/codebuddy'` | bundle 分流未命中 | 设 `CODEBUDDY_FORCE_HEADLESS_BUNDLE=1` |
|
|
||||||
| `Authentication required. Please use /login` | 无凭据 | 按 §3 填 key |
|
|
||||||
| `ready:false` 但 key 已填 | `internetEnvironment` 版本不对 | 中国版必须是 `internal` |
|
|
||||||
| HTTP 层全正常、agent 却不干活 | 典型的 EADDRINUSE 假象 | 看 §5.1 |
|
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,9 @@
|
|||||||
|
# 2026-10-08 工作日志
|
||||||
|
|
||||||
|
## 榜单数据更新(自动化 automation-1791425215326)
|
||||||
|
|
||||||
|
- 按 mcn-data-insight「榜单数据落盘规范」抓取热点数据四榜最新一期,落盘目录:`C:\Users\maidou\Desktop\MCNSkill项目\三方数据\红狐数据\抖音榜单\`
|
||||||
|
- 写入 8 个文件:账号日榜 1(数据期 2026-10-06)、账号周榜 5 赛道(数据期 2026-09-28~10-04)、视频热榜 1(周标号 2026-09-28)、点赞榜 1(周标号 2026-09-28),各 50 条
|
||||||
|
- 实际 API 调用 8 次(hot1 + likes1 + 日榜1 + 周榜5),无缓存命中(上一期:日榜 09-26、周榜 09-07、视频/点赞 09-21)
|
||||||
|
- 运行要点:`fetch_week_ranks.py` 需 cd 到 scripts 目录用 `D:/miniconda3/python.exe` 跑;`fetch_rank.py` 先落临时目录再按 JSON 内 dateStart 重命名到正式文件名
|
||||||
|
- 注意:Bash `mktemp` 得到的是 /tmp 路径,传给 Python 前需 `cygpath -w` 转 Windows 路径,否则 FileNotFoundError
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+13
@@ -0,0 +1,13 @@
|
|||||||
|
# automation-1790682160345 执行记录
|
||||||
|
|
||||||
|
## 任务性质
|
||||||
|
链路自检(健康检查)任务。指令:仅回复「链路正常」四字。
|
||||||
|
|
||||||
|
## 历史执行
|
||||||
|
| 时间 | 结果 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 2026-09-29 19:43 | 成功 | 定时任务正常触发并返回,链路通畅。首次执行,无历史记录文件,已初始化本文件。 |
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
- 该任务不产出任何交付文件,无需 present_files。
|
||||||
|
- 只做链路连通性验证:定时调度 → 会话唤起 → 模型响应 全链路可达。
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
# automation-1791425215326 · 榜单数据更新(热点数据四榜落盘)
|
||||||
|
|
||||||
|
## 执行约定(复用)
|
||||||
|
- 落盘目录:`C:\Users\maidou\Desktop\MCNSkill项目\三方数据\红狐数据\抖音榜单\`(桌面未 OneDrive 重定向;env `MCN_RANKING_DIR` 可覆盖)
|
||||||
|
- 四榜:账号日榜(全品类 1 次)/账号周榜(5 赛道:生活vlog 小剧场 亲子 美食 旅行)/视频热榜/点赞榜(各 1 次,不传赛道)
|
||||||
|
- 幂等:调用前先 ls 目标子目录,同榜名+赛道+数据日期已存在则跳过
|
||||||
|
- Python 用 `D:/miniconda3/python.exe`;`fetch_week_ranks.py` 在 scripts 目录执行;`fetch_rank.py` 先写临时目录,按 JSON 内 dateStart 定正式文件名
|
||||||
|
|
||||||
|
## 执行历史
|
||||||
|
| 运行日期 | 数据期 | 写入 | API 调用 | 备注 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 2026-10-08 | 日榜 2026-10-06;周榜/热榜/点赞 2026-09-28 | 8 个文件(各 50 条) | 8 次 | 该自动化首次记录执行;上一期缓存为日榜 09-26 / 周榜 09-07 / 视频·点赞 09-21,全部未命中故全量抓取 |
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
# automation-1791433363165 执行记录
|
||||||
|
|
||||||
|
## 2026-10-08 12:23
|
||||||
|
- 任务:按用户要求只回复两个字「成功」。
|
||||||
|
- 结果:已按字面要求输出「成功」,无文件产出。
|
||||||
|
- 备注:本次为首次执行,无历史记录。
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+119
-12
@@ -69,6 +69,50 @@ function resolveCli(explicitPath) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- 凭据解析
|
||||||
|
|
||||||
|
// 凭据默认落点:**刻意放在仓库之外**。
|
||||||
|
// config.json 是 git 跟踪文件,绝不能让 key 落进去;本目录也不会被技能同步脚本复制。
|
||||||
|
function defaultCredentialFile() {
|
||||||
|
return path.join(os.homedir(), '.workbuddy', 'mcn-workshop', 'ai-cli.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读凭据文件:{"apiKey":"...","authToken":"..."}
|
||||||
|
function readCredentialFile(p) {
|
||||||
|
if (!p) return {};
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(p)) return {};
|
||||||
|
const j = JSON.parse(fs.readFileSync(p, 'utf8'));
|
||||||
|
return {
|
||||||
|
apiKey: typeof j.apiKey === 'string' ? j.apiKey.trim() : '',
|
||||||
|
authToken: typeof j.authToken === 'string' ? j.authToken.trim() : '',
|
||||||
|
file: p,
|
||||||
|
};
|
||||||
|
} catch (e) { return {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 凭据优先级(高 → 低):
|
||||||
|
* 1) 调用方显式传入(config.json 的 ai.cli.apiKey / authToken)
|
||||||
|
* 2) 凭据文件(默认 ~/.workbuddy/mcn-workshop/ai-cli.json)
|
||||||
|
* 3) 环境变量 CODEBUDDY_API_KEY / CODEBUDDY_AUTH_TOKEN
|
||||||
|
* 返回 { apiKey, authToken, source }
|
||||||
|
*/
|
||||||
|
function resolveCredentials(o) {
|
||||||
|
o = o || {};
|
||||||
|
const f = readCredentialFile(o.credentialFile || defaultCredentialFile());
|
||||||
|
const apiKey = o.apiKey || f.apiKey || process.env.CODEBUDDY_API_KEY || '';
|
||||||
|
const authToken = o.authToken || f.authToken || process.env.CODEBUDDY_AUTH_TOKEN || '';
|
||||||
|
let source = '(未配置)';
|
||||||
|
if (o.authToken) source = 'config:authToken';
|
||||||
|
else if (o.apiKey) source = 'config:apiKey';
|
||||||
|
else if (f.authToken) source = 'file:authToken ' + f.file;
|
||||||
|
else if (f.apiKey) source = 'file:apiKey ' + f.file;
|
||||||
|
else if (process.env.CODEBUDDY_AUTH_TOKEN) source = 'env:CODEBUDDY_AUTH_TOKEN';
|
||||||
|
else if (process.env.CODEBUDDY_API_KEY) source = 'env:CODEBUDDY_API_KEY';
|
||||||
|
return { apiKey, authToken, source, credentialFile: f.file || defaultCredentialFile() };
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- 环境构造
|
// ---------------------------------------------------------------- 环境构造
|
||||||
|
|
||||||
// 把「宿主 WorkBuddy 注入的、会干扰子 CLI 的」环境变量剔除,并注入凭据
|
// 把「宿主 WorkBuddy 注入的、会干扰子 CLI 的」环境变量剔除,并注入凭据
|
||||||
@@ -86,6 +130,9 @@ function buildChildEnv(opts) {
|
|||||||
for (const k of Object.keys(env)) {
|
for (const k of Object.keys(env)) {
|
||||||
if (k.startsWith('CODEBUDDY_SESSION_') || k.startsWith('CODEBUDDY_CONVERSATION_') ||
|
if (k.startsWith('CODEBUDDY_SESSION_') || k.startsWith('CODEBUDDY_CONVERSATION_') ||
|
||||||
k === 'CODEBUDDY_TOOL_CALL_ID' || k === 'CODEBUDDY_SERVICE_PROXY_URL' ||
|
k === 'CODEBUDDY_TOOL_CALL_ID' || k === 'CODEBUDDY_SERVICE_PROXY_URL' ||
|
||||||
|
k === 'CODEBUDDY_MCP_CONFIG' || // 宿主 MCP(weixinpay/sheetagent…)我们不要
|
||||||
|
k === 'CODEBUDDY_GATEWAY_PASSWORD' || k === 'CODEBUDDY_GATEWAY_AUTH' ||
|
||||||
|
k === 'WORKBUDDY_PAC_RPC_TOKEN' || k === 'WORKBUDDY_PAC_RPC_SOCKET' ||
|
||||||
k === 'SANDBOX_CENTER_IPC_ADDRESS' || k === 'BAGGAGE' || k === 'BASH_ENV') {
|
k === 'SANDBOX_CENTER_IPC_ADDRESS' || k === 'BAGGAGE' || k === 'BASH_ENV') {
|
||||||
delete env[k];
|
delete env[k];
|
||||||
}
|
}
|
||||||
@@ -97,12 +144,17 @@ function buildChildEnv(opts) {
|
|||||||
env.CODEBUDDY_SAFE_DELETE_ENABLED = '0'; // 工作台场景不需要工具删除保护
|
env.CODEBUDDY_SAFE_DELETE_ENABLED = '0'; // 工作台场景不需要工具删除保护
|
||||||
env.CODEBUDDY_GATEWAY_DISABLE_API_DOCS = '1';
|
env.CODEBUDDY_GATEWAY_DISABLE_API_DOCS = '1';
|
||||||
env.CODEBUDDY_DISABLE_TELEMETRY = '1';
|
env.CODEBUDDY_DISABLE_TELEMETRY = '1';
|
||||||
|
// 官方建议:纯 -p 单发不支持后台任务,开着会返回明确错误
|
||||||
|
env.CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS = '1';
|
||||||
|
|
||||||
// 【坑 3】凭据:独立进程必须自备
|
// 【坑 3】凭据:独立进程必须自备。
|
||||||
const key = opts.apiKey || process.env.CODEBUDDY_API_KEY || '';
|
// 注意:宿主也会用同名变量注入凭据且读后即删——必须先把继承来的残留清掉,再写我们自己的,
|
||||||
const token = opts.authToken || process.env.CODEBUDDY_AUTH_TOKEN || '';
|
// 否则可能带着一个「空壳/过期」的值进入子进程。
|
||||||
if (token) env.CODEBUDDY_AUTH_TOKEN = token; // 优先级最高
|
delete env.CODEBUDDY_API_KEY;
|
||||||
if (key) env.CODEBUDDY_API_KEY = key;
|
delete env.CODEBUDDY_AUTH_TOKEN;
|
||||||
|
const cred = resolveCredentials(opts);
|
||||||
|
if (cred.authToken) env.CODEBUDDY_AUTH_TOKEN = cred.authToken; // 优先级最高
|
||||||
|
if (cred.apiKey) env.CODEBUDDY_API_KEY = cred.apiKey;
|
||||||
if (opts.internetEnvironment) env.CODEBUDDY_INTERNET_ENVIRONMENT = opts.internetEnvironment;
|
if (opts.internetEnvironment) env.CODEBUDDY_INTERNET_ENVIRONMENT = opts.internetEnvironment;
|
||||||
if (opts.baseUrl) env.CODEBUDY_BASE_URL = opts.baseUrl;
|
if (opts.baseUrl) env.CODEBUDY_BASE_URL = opts.baseUrl;
|
||||||
|
|
||||||
@@ -123,11 +175,17 @@ function buildChildEnv(opts) {
|
|||||||
* @param {string} o.cwd 工作目录(决定 CLI 能看到哪些项目文件)
|
* @param {string} o.cwd 工作目录(决定 CLI 能看到哪些项目文件)
|
||||||
* @param {string} [o.homeDir] CLI 的配置/数据目录(默认 <os.tmpdir>/mcn-cli-home)
|
* @param {string} [o.homeDir] CLI 的配置/数据目录(默认 <os.tmpdir>/mcn-cli-home)
|
||||||
* @param {string} [o.model] 模型 ID
|
* @param {string} [o.model] 模型 ID
|
||||||
* @param {string} [o.apiKey] CODEBUDDY_API_KEY
|
* @param {string} [o.apiKey] CODEBUDDY_API_KEY(优先级最高,一般留空走凭据文件)
|
||||||
* @param {string} [o.authToken] CODEBUDDY_AUTH_TOKEN
|
* @param {string} [o.authToken] CODEBUDDY_AUTH_TOKEN
|
||||||
|
* @param {string} [o.credentialFile] 凭据文件路径,默认 ~/.workbuddy/mcn-workshop/ai-cli.json
|
||||||
* @param {string} [o.internetEnvironment] internal / ioa / 留空
|
* @param {string} [o.internetEnvironment] internal / ioa / 留空
|
||||||
* @param {string} [o.cliPath] 显式指定 CLI 路径
|
* @param {string} [o.cliPath] 显式指定 CLI 路径
|
||||||
* @param {number} [o.timeoutMs]
|
* @param {number} [o.timeoutMs]
|
||||||
|
* @param {boolean} [o.search] 是否允许联网搜索(默认 true)。true → 工具面收敛为 WebSearch+WebFetch
|
||||||
|
* @param {string} [o.tools] 显式工具白名单:'default' | 'none' | 'WebSearch,WebFetch'(留空=按 search 推导)
|
||||||
|
* @param {boolean} [o.strictMcp] 是否屏蔽宿主 MCP(默认 true)
|
||||||
|
* @param {string} [o.appendSystemPrompt] 追加系统提示(如搜索使用策略)
|
||||||
|
* @param {number} [o.maxTurns] 最大 agent 轮数(0=不限)
|
||||||
* @param {function} o.onDelta (text) => void 文本增量
|
* @param {function} o.onDelta (text) => void 文本增量
|
||||||
* @param {function} [o.onThink] (text) => void 思考增量(可选)
|
* @param {function} [o.onThink] (text) => void 思考增量(可选)
|
||||||
* @param {function} [o.onEvent] (obj) => void 全部原始事件(可选,调试用)
|
* @param {function} [o.onEvent] (obj) => void 全部原始事件(可选,调试用)
|
||||||
@@ -154,6 +212,33 @@ function runCli(o) {
|
|||||||
...(o.permissionMode ? ['--permission-mode', o.permissionMode] : []),
|
...(o.permissionMode ? ['--permission-mode', o.permissionMode] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ---- 工具面收敛(既提速,也是安全边界)----
|
||||||
|
// 工作台的 AI 只负责「产出文本」,prompt 里已自带全部上下文(账号设定/需求/历史),
|
||||||
|
// 不需要读写文件、跑命令。默认只给联网搜索两个工具:
|
||||||
|
// · 启动快(不必加载 Bash/Edit/Write/... 的工具 schema)
|
||||||
|
// · 模型物理上碰不到本机文件系统
|
||||||
|
// o.search === false → 放开默认工具面,但明确禁用联网搜索
|
||||||
|
let tools = o.tools;
|
||||||
|
if (tools === undefined || tools === null || tools === '') {
|
||||||
|
tools = (o.search === false) ? 'default' : 'WebSearch,WebFetch';
|
||||||
|
}
|
||||||
|
if (tools === 'none') args.push('--tools', '');
|
||||||
|
else if (tools && tools !== 'default') args.push('--tools', tools);
|
||||||
|
if (o.search === false) args.push('--disallowedTools', 'WebSearch,WebFetch');
|
||||||
|
|
||||||
|
// ---- 屏蔽宿主 MCP ----
|
||||||
|
// 宿主会把 CODEBUDDY_MCP_CONFIG 注入进程树,里面挂着 weixinpay / sheetagent 等。
|
||||||
|
// 工作台场景一个都用不上,加载它们纯属浪费启动时间,还可能被模型误调。
|
||||||
|
if (o.strictMcp !== false) {
|
||||||
|
args.push('--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 推理开销:把首字延迟从几十秒压到十几秒的关键开关(minimal|low|medium|high|xhigh|max)
|
||||||
|
if (o.effort) args.push('--effort', o.effort);
|
||||||
|
|
||||||
|
if (o.appendSystemPrompt) args.push('--append-system-prompt', o.appendSystemPrompt);
|
||||||
|
if (parseInt(o.maxTurns, 10) > 0) args.push('--max-turns', String(parseInt(o.maxTurns, 10)));
|
||||||
|
|
||||||
const env = buildChildEnv({
|
const env = buildChildEnv({
|
||||||
homeDir,
|
homeDir,
|
||||||
apiKey: o.apiKey,
|
apiKey: o.apiKey,
|
||||||
@@ -183,6 +268,7 @@ function runCli(o) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let full = '';
|
let full = '';
|
||||||
|
let streamed = ''; // 已经通过 onDelta 推给前端的内容(用于终态比对)
|
||||||
let stderr = '';
|
let stderr = '';
|
||||||
let sessionId = '';
|
let sessionId = '';
|
||||||
let usage = null;
|
let usage = null;
|
||||||
@@ -190,6 +276,11 @@ function runCli(o) {
|
|||||||
let errText = '';
|
let errText = '';
|
||||||
let sawDelta = false;
|
let sawDelta = false;
|
||||||
let buf = '';
|
let buf = '';
|
||||||
|
// 每次工具调用的发生位置(= 当时的 full 长度)。
|
||||||
|
// 根因:模型在发起 WebSearch 前常先吐一句过程话术(如
|
||||||
|
// "I'll search for ... before responding."),它不是答案。
|
||||||
|
// 只保留「最后一次工具调用之后」的文本作为正文。
|
||||||
|
const toolMarks = [];
|
||||||
|
|
||||||
const handleLine = (line) => {
|
const handleLine = (line) => {
|
||||||
line = line.trim();
|
line = line.trim();
|
||||||
@@ -206,6 +297,12 @@ function runCli(o) {
|
|||||||
// 文本增量:stream_event → content_block_delta → text_delta
|
// 文本增量:stream_event → content_block_delta → text_delta
|
||||||
if (ev.type === 'stream_event' && ev.event) {
|
if (ev.type === 'stream_event' && ev.event) {
|
||||||
const e2 = ev.event;
|
const e2 = ev.event;
|
||||||
|
// 工具调用开始的边界(用于切掉调用前的过程话术)
|
||||||
|
if (e2.type === 'content_block_start' && e2.content_block &&
|
||||||
|
(e2.content_block.type === 'tool_use' || e2.content_block.name)) {
|
||||||
|
toolMarks.push(full.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (e2.type === 'content_block_delta' && e2.delta) {
|
if (e2.type === 'content_block_delta' && e2.delta) {
|
||||||
if (e2.delta.type === 'text_delta' && e2.delta.text) {
|
if (e2.delta.type === 'text_delta' && e2.delta.text) {
|
||||||
const prev = full;
|
const prev = full;
|
||||||
@@ -219,6 +316,7 @@ function runCli(o) {
|
|||||||
// 已误判过一次的话,本段的累积都作废
|
// 已误判过一次的话,本段的累积都作废
|
||||||
if (isCliBoilerplate(prev)) { full = ''; errText = errText || prev; return; }
|
if (isCliBoilerplate(prev)) { full = ''; errText = errText || prev; return; }
|
||||||
sawDelta = true;
|
sawDelta = true;
|
||||||
|
streamed += e2.delta.text;
|
||||||
if (o.onDelta) { try { o.onDelta(e2.delta.text); } catch (e) {} }
|
if (o.onDelta) { try { o.onDelta(e2.delta.text); } catch (e) {} }
|
||||||
} else if (e2.delta.type === 'thinking_delta' && e2.delta.thinking && o.onThink) {
|
} else if (e2.delta.type === 'thinking_delta' && e2.delta.thinking && o.onThink) {
|
||||||
try { o.onThink(e2.delta.thinking); } catch (e) {}
|
try { o.onThink(e2.delta.thinking); } catch (e) {}
|
||||||
@@ -229,6 +327,10 @@ function runCli(o) {
|
|||||||
|
|
||||||
// 兜底:未开启/未产生增量流时,整段文本在 assistant 消息里
|
// 兜底:未开启/未产生增量流时,整段文本在 assistant 消息里
|
||||||
if (ev.type === 'assistant' && ev.message && Array.isArray(ev.message.content)) {
|
if (ev.type === 'assistant' && ev.message && Array.isArray(ev.message.content)) {
|
||||||
|
// 工具调用边界(未走增量流时,tool_use 只会出现在 assistant 消息里)
|
||||||
|
for (const c of ev.message.content) {
|
||||||
|
if (c && c.type === 'tool_use') toolMarks.push(full.length);
|
||||||
|
}
|
||||||
if (!sawDelta) {
|
if (!sawDelta) {
|
||||||
const t = ev.message.content
|
const t = ev.message.content
|
||||||
.filter((c) => c && c.type === 'text' && typeof c.text === 'string')
|
.filter((c) => c && c.type === 'text' && typeof c.text === 'string')
|
||||||
@@ -236,6 +338,7 @@ function runCli(o) {
|
|||||||
if (t && !full) {
|
if (t && !full) {
|
||||||
if (isCliBoilerplate(t)) { errText = errText || t; return; }
|
if (isCliBoilerplate(t)) { errText = errText || t; return; }
|
||||||
full = t;
|
full = t;
|
||||||
|
streamed += t;
|
||||||
if (o.onDelta) { try { o.onDelta(t); } catch (e) {} }
|
if (o.onDelta) { try { o.onDelta(t); } catch (e) {} }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,18 +386,22 @@ function runCli(o) {
|
|||||||
child.on('close', (code) => {
|
child.on('close', (code) => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
if (buf.trim()) handleLine(buf);
|
if (buf.trim()) handleLine(buf);
|
||||||
const text = full || resultText;
|
// 切掉「最后一次工具调用之前」的过程话术:那部分不是答案。
|
||||||
if (aborted) return resolve({ ok: false, text, error: 'CLI 执行超时或被取消', sessionId, usage });
|
// (例:模型先输出 "I'll search for ... before responding." 再发起 WebSearch)
|
||||||
|
const cut = toolMarks.length ? toolMarks[toolMarks.length - 1] : 0;
|
||||||
|
const answer = (cut > 0 && cut < full.length) ? full.slice(cut).trim() : full;
|
||||||
|
const text = answer || resultText;
|
||||||
|
if (aborted) return resolve({ ok: false, text, streamed, error: 'CLI 执行超时或被取消', sessionId, usage });
|
||||||
// 事件里明确报了错(如未认证)→ 优先按错误返回
|
// 事件里明确报了错(如未认证)→ 优先按错误返回
|
||||||
if (errText) {
|
if (errText) {
|
||||||
const raw = errText;
|
const raw = errText;
|
||||||
return resolve({ ok: false, text, error: messageForCliError(raw) || raw, sessionId, usage, rawStderr: raw });
|
return resolve({ ok: false, text, streamed, error: messageForCliError(raw) || raw, sessionId, usage, rawStderr: raw });
|
||||||
}
|
}
|
||||||
if (code !== 0 && !text) {
|
if (code !== 0 && !text) {
|
||||||
const msg = (stderr || '').trim().split('\n').filter(Boolean).slice(-3).join(' | ');
|
const msg = (stderr || '').trim().split('\n').filter(Boolean).slice(-3).join(' | ');
|
||||||
return resolve({ ok: false, text, error: messageForCliError(msg) || ('CLI 退出码 ' + code), sessionId, usage, rawStderr: msg });
|
return resolve({ ok: false, text, streamed, error: messageForCliError(msg) || ('CLI 退出码 ' + code), sessionId, usage, rawStderr: msg });
|
||||||
}
|
}
|
||||||
resolve({ ok: true, text, sessionId, usage });
|
resolve({ ok: true, text, streamed, sessionId, usage });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -324,4 +431,4 @@ function messageForCliError(msg) {
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { runCli, resolveCli, buildChildEnv };
|
module.exports = { runCli, resolveCli, buildChildEnv, resolveCredentials, defaultCredentialFile };
|
||||||
+11
-3
@@ -4,17 +4,25 @@
|
|||||||
"outputRoot": "MCNSkill项目",
|
"outputRoot": "MCNSkill项目",
|
||||||
"businessDataDir": ["MCNSkill项目", "三方数据", "红狐数据", "抖音榜单"],
|
"businessDataDir": ["MCNSkill项目", "三方数据", "红狐数据", "抖音榜单"],
|
||||||
"extraRoots": [],
|
"extraRoots": [],
|
||||||
|
"sessionCwd": "D:\\AI技能\\mcn-workshop",
|
||||||
|
|
||||||
"ai": {
|
"ai": {
|
||||||
"backend": "dify",
|
"backend": "cli",
|
||||||
"cli": {
|
"cli": {
|
||||||
"path": "",
|
"path": "",
|
||||||
"model": "custom:doubao-seed-2-1-pro-260628",
|
"model": "deepseek-v4.1-flash",
|
||||||
|
"effort": "low",
|
||||||
"apiKey": "",
|
"apiKey": "",
|
||||||
"authToken": "",
|
"authToken": "",
|
||||||
|
"credentialFile": "",
|
||||||
"internetEnvironment": "internal",
|
"internetEnvironment": "internal",
|
||||||
"permissionMode": "",
|
"permissionMode": "",
|
||||||
"timeoutMs": 300000
|
"timeoutMs": 300000,
|
||||||
|
"search": true,
|
||||||
|
"tools": "",
|
||||||
|
"strictMcp": true,
|
||||||
|
"appendSystemPrompt": "",
|
||||||
|
"maxTurns": 6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-2
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## 触发链路(所有入口共用)
|
## 触发链路(所有入口共用)
|
||||||
|
|
||||||
按钮点击 → 确认弹窗(confirmRun / confirmAiScript / confirmSaveAccount)→ `executeTask()` → `POST /api/run`(server.js 写 automations 表,scheduledAt=now+5s + next_run_at=now+5000,`skills` 数组→`skills_json` 参数级技能挂载)→ 客户端调度器拾取 → 左侧会话栏「mcn-work-shop」空间后台会话执行(cwd = 工作台目录,由 `server.js` 从 `ROOT` 动态推导)→ 轮询 `/api/run/status`(done/error/review)→ 完成刷新页面。
|
按钮点击 → 确认弹窗(confirmRun / confirmAiScript / confirmSaveAccount)→ `executeTask()` → `POST /api/run`(server.js 写 automations 表,scheduledAt=now+5s + next_run_at=now+5000,`skills` 数组→`skills_json` 参数级技能挂载)→ 客户端调度器拾取 → 左侧会话栏「mcn-workshop」空间后台会话执行(cwd = 工作台目录,由 `server.js` 从 `ROOT` 动态推导)→ 轮询 `/api/run/status`(done/error/review)→ 完成刷新页面。
|
||||||
|
|
||||||
## 功能入口 × 技能调用契约总表
|
## 功能入口 × 技能调用契约总表
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
| 候选 | `==OPTS==` | 每行一个候选,格式「骨架词——具体展开」(≤45 字、无编号);结合本条创意/人设落地,禁干巴术语 | 解析为气泡下内嵌按钮行,点选=以整行文本发一轮 |
|
| 候选 | `==OPTS==` | 每行一个候选,格式「骨架词——具体展开」(≤45 字、无编号);结合本条创意/人设落地,禁干巴术语 | 解析为气泡下内嵌按钮行,点选=以整行文本发一轮 |
|
||||||
| 需求 | `==REQ==` | 8 行清单:内容形式/一句话故事/开场钩子/故事爆点/故事爽点/故事框架/人物关系/其他要求(空项留空) | 解析回填需求框字段 |
|
| 需求 | `==REQ==` | 8 行清单:内容形式/一句话故事/开场钩子/故事爆点/故事爽点/故事框架/人物关系/其他要求(空项留空) | 解析回填需求框字段 |
|
||||||
|
|
||||||
**System Prompt 已抽为文件**:`mcn-work-shop/prompts/clarify.md`(==PROMPT== 与 ==CHANGELOG== 之间为正文,头尾说明不发模型)。**迭代 prompt 只改该文件**,server.js 启动按 mtime 缓存读取、占位符注入(`{{accountName}}/{{personaText}}/{{contentText}}/{{attachText}}/{{reqMdText}}/{{histText}}/{{turnText}}`),不再改 server.js 字符串。
|
**System Prompt 已抽为文件**:`mcn-workshop/prompts/clarify.md`(==PROMPT== 与 ==CHANGELOG== 之间为正文,头尾说明不发模型)。**迭代 prompt 只改该文件**,server.js 启动按 mtime 缓存读取、占位符注入(`{{accountName}}/{{personaText}}/{{contentText}}/{{attachText}}/{{reqMdText}}/{{histText}}/{{turnText}}`),不再改 server.js 字符串。
|
||||||
|
|
||||||
**关键规则(clarify.md 内固化)**:
|
**关键规则(clarify.md 内固化)**:
|
||||||
- 候选骨架受控词表:内容形式(剧情/Vlog/口播/**视觉短片**)、故事框架(钩子前置/悬念反转/递进铺垫/先抑后扬)、开场钩子(悬念提问/反差画面/直击痛点/身份共鸣/利益承诺)、剧情人物关系(一次一层);每项必须结合本条创意给「具体形态」
|
- 候选骨架受控词表:内容形式(剧情/Vlog/口播/**视觉短片**)、故事框架(钩子前置/悬念反转/递进铺垫/先抑后扬)、开场钩子(悬念提问/反差画面/直击痛点/身份共鸣/利益承诺)、剧情人物关系(一次一层);每项必须结合本条创意给「具体形态」
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
# AI 链路:本地 CodeBuddy CLI 接入(A 方案)
|
||||||
|
|
||||||
|
> 2026-09-29 落地并实测通过。工作台的 AI 能力(需求打磨 / 创作)除 Dify 外,新增**本地 CodeBuddy CLI** 通路。
|
||||||
|
> 实现:`cli-backend.js`(后端模块)+ `server.js` 的 `/api/ai/clarify`、`/api/ai/status`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 一句话
|
||||||
|
|
||||||
|
把「工作台 → 模型」从 **HTTP 调 Dify** 换成 **spawn 一个 `codebuddy` 子进程、用 stdio 通信**。
|
||||||
|
**不涉及任何本地端口**,与 WorkBuddy 桌面程序完全解耦。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 为什么必须自备凭据(别再挖宿主那条路)
|
||||||
|
|
||||||
|
排查中钉死了三条硬结论:
|
||||||
|
|
||||||
|
| 结论 | 证据 |
|
||||||
|
|:--|:--|
|
||||||
|
| 宿主 gateway 端口/密码**每轮轮换** | 同机同天实测端口 `12134 → 13116 → 10261`;密码同步换 |
|
||||||
|
| 唯一发现通道只存在于 WorkBuddy 派生进程树内 | `CODEBUDDY_SERVICE_PROXY_URL`;用户双击 `start.bat` 起的工作台拿不到 |
|
||||||
|
| WorkBuddy **故意**不让子进程继承凭据 | 4 个环境变量交付 → CLI 读完**立即 `delete`**;凭据不落明文盘 |
|
||||||
|
|
||||||
|
→ 走官方文档给的非交互认证通道:`CODEBUDDY_API_KEY` / `CODEBUDDY_AUTH_TOKEN`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 配置步骤
|
||||||
|
|
||||||
|
### 步骤 1:拿 API Key
|
||||||
|
|
||||||
|
| 版本 | 地址 |
|
||||||
|
|:--|:--|
|
||||||
|
| **中国版**(本机是这版,`internetEnvironment=internal`) | https://copilot.tencent.com/profile/ |
|
||||||
|
| 国际版 | https://www.codebuddy.ai/profile/keys |
|
||||||
|
| iOA | https://tencent.sso.copilot.tencent.com/profile/keys |
|
||||||
|
|
||||||
|
### 步骤 2:把 Key 放进凭据文件(⛔ 不要写进 config.json)
|
||||||
|
|
||||||
|
**`config.json` 是 git 跟踪文件,写进去会提交到仓库。**
|
||||||
|
|
||||||
|
默认凭据文件(仓库之外,不会被技能同步脚本复制):
|
||||||
|
|
||||||
|
```
|
||||||
|
%USERPROFILE%\.workbuddy\mcn-workshop\ai-cli.json
|
||||||
|
```
|
||||||
|
|
||||||
|
内容:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "apiKey": "ck_xxxxxxxx._xxxxxxxx" }
|
||||||
|
```
|
||||||
|
|
||||||
|
也可用 `authToken` 字段(优先级更高)。或直接设环境变量 `CODEBUDDY_API_KEY`。
|
||||||
|
|
||||||
|
### 步骤 3:自检
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8900/api/ai/status" # 只报告配置,零消耗
|
||||||
|
curl "http://localhost:8900/api/ai/status?probe=1" # 真跑一次极短提示
|
||||||
|
```
|
||||||
|
|
||||||
|
期望 `"ready": true` 且 `probe.ok = true`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 字段说明(`config.json` → `ai`)
|
||||||
|
|
||||||
|
| 字段 | 默认 | 说明 |
|
||||||
|
|:--|:--|:--|
|
||||||
|
| `backend` | `cli` | `"cli"` 走本地 CLI;`"dify"` 走原链路。环境变量 `MCN_AI_BACKEND` 可临时覆盖 |
|
||||||
|
| `cli.path` | `''` | 留空自动探测:`CODEBUDDY_CLI_PATH` → WorkBuddy 自带 CLI → `PATH` |
|
||||||
|
| `cli.model` | `deepseek-v4.1-flash` | **只影响 `/api/ai/clarify`**(需求打磨)。环境变量 `MCN_AI_MODEL` 可临时覆盖。⚠️ 创作任务的模型另见 §11 |
|
||||||
|
| `cli.effort` | `low` | 推理档位 `minimal\|low\|medium\|high\|xhigh\|max`。**首字延迟第一大杠杆** |
|
||||||
|
| `cli.search` | `true` | 是否允许联网搜索(见 §7) |
|
||||||
|
| `cli.tools` | `''` | 工具白名单。留空=按 `search` 推导;`none`=全禁;也可写 `WebSearch,WebFetch` |
|
||||||
|
| `cli.strictMcp` | `true` | 屏蔽宿主 MCP(weixinpay / sheetagent 等) |
|
||||||
|
| `cli.maxTurns` | `6` | agent 轮数硬上限,防搜索失控跑飞 |
|
||||||
|
| `cli.appendSystemPrompt` | `''` | 追加系统提示;留空时自动注入内置搜索策略(`CLI_SEARCH_HINT`) |
|
||||||
|
| `cli.credentialFile` | `''` | 凭据文件路径;留空走默认位置 |
|
||||||
|
| `cli.apiKey` / `cli.authToken` | `''` | 优先级最高,**但别填**(会被 git 记录) |
|
||||||
|
| `cli.timeoutMs` | `300000` | 单次调用超时 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 三条必须保留的防护(改代码前务必读)
|
||||||
|
|
||||||
|
`cli-backend.js` 的 `buildChildEnv()` 做了三件事,每条都是实测踩出来的坑:
|
||||||
|
|
||||||
|
### 5.1 删掉 `SERVER__PORT` / `SERVER__HOST`
|
||||||
|
|
||||||
|
`codebuddy`(**连 `-p` 打印模式也**)内部会起一个 HTTP server:
|
||||||
|
|
||||||
|
```js
|
||||||
|
listen(parseInt(process.env.SERVER__PORT) || config.get("cell.server", { port: 3000 }).port)
|
||||||
|
```
|
||||||
|
|
||||||
|
在 WorkBuddy 进程树内启动 → 继承宿主的 `SERVER__PORT` → `EADDRINUSE` → **进程静默卡死**。
|
||||||
|
迷惑性极强:HTTP 层(health / info / sessions)全正常,只有 agent 执行环节挂,
|
||||||
|
极易被误判成「prewarm 占端口」。
|
||||||
|
|
||||||
|
官方自己也这么处理:`delete el.SERVER__PORT, delete el.SERVER__HOST`。
|
||||||
|
|
||||||
|
### 5.2 强制 `CODEBUDDY_FORCE_HEADLESS_BUNDLE=1`
|
||||||
|
|
||||||
|
`bin/codebuddy` launcher 按环境变量选 bundle:
|
||||||
|
|
||||||
|
| 环境变量 | 命中产物 |
|
||||||
|
|:--|:--|
|
||||||
|
| `CODEBUDDY_FORCE_LITE_WB_BUNDLE=1`(宿主注入,**优先级最高**) | `codebuddy-lite-wb.mjs`(WorkBuddy 特供精简包) |
|
||||||
|
| `CODEBUDDY_FORCE_HEADLESS_BUNDLE=1` | `codebuddy-headless.js`(**我们要的**) |
|
||||||
|
| 含 `--print` | 自动 headless |
|
||||||
|
| 都没有 | `dist/codebuddy` → **本安装不存在**,报 `MODULE_NOT_FOUND` |
|
||||||
|
|
||||||
|
### 5.3 清掉宿主注入的会话上下文
|
||||||
|
|
||||||
|
`NODE_OPTIONS`(node-language-shim)、`CODEBUDDY_SESSION_*`、`CODEBUDDY_MCP_CONFIG`、
|
||||||
|
`CODEBUDDY_GATEWAY_PASSWORD`、`BAGGAGE` 等一并删除,并把 `CODEBUDDY_CONFIG_DIR`
|
||||||
|
指向独立目录(默认 `<tmpdir>/mcn-cli-home`),避免污染 WorkBuddy 自身配置。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 性能:三个真正的杠杆(实测数据)
|
||||||
|
|
||||||
|
| 杠杆 | 做法 | 效果 |
|
||||||
|
|:--|:--|:--|
|
||||||
|
| **① 工具面收敛** | `--tools "WebSearch,WebFetch"` | prompt 从 **21,776 → 4,859 tokens(-78%)**,耗时 **47.6s → 15.0s** |
|
||||||
|
| **② 推理档位** | `--effort low` | 首字延迟 **61.7s → 19.7s** |
|
||||||
|
| **③ 换模型** | 见 §9 | `deepseek-v4-pro` 46.2s vs `Claude-Opus-4.8` 18.5s(同一提示词) |
|
||||||
|
|
||||||
|
> ⚠️ `[INIT tools]` 事件里列的是**工具注册表全量**,不是实际启用集。
|
||||||
|
> 判断 `--tools` 是否生效要看 `usage.input_tokens`,别被事件列表误导。
|
||||||
|
|
||||||
|
**额外收益**:工具面收敛后模型**物理上碰不到文件系统**(`Bash`/`Read`/`Write`/`Edit` 全不在白名单),
|
||||||
|
这是免费拿到的一道安全边界。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 联网搜索
|
||||||
|
|
||||||
|
CLI **原生带** `WebSearch` / `WebFetch`,开箱可用,无需额外配置。实测:
|
||||||
|
|
||||||
|
```
|
||||||
|
[TOOL_USE] WebSearch {"query":"今天热点新闻","freshness":"d1"}
|
||||||
|
```
|
||||||
|
|
||||||
|
但搜索会把耗时推到 1–2 分钟,所以**不能简单地常开或常关**,而是给模型一条明确的判据。
|
||||||
|
`server.js` 的 `CLI_SEARCH_HINT` 会作为 `--append-system-prompt` 注入:
|
||||||
|
|
||||||
|
- **该搜**:回答依赖「今天/近期」的外部事实(当下热点、平台规则与趋势、行业数据、事实核查)
|
||||||
|
- **不该搜**:创作讨论、需求澄清、脚本撰写、文案打磨 → 一律不搜
|
||||||
|
- **搜索预算**:最多 1 次 WebSearch(+ 必要 1 次 WebFetch),硬上限 2 次;搜到即答,禁止反复搜
|
||||||
|
- 失败/超时直接凭已有信息作答,不重试
|
||||||
|
|
||||||
|
想完全关闭:`"search": false`(会自动改用 `--disallowedTools WebSearch,WebFetch`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 输出协议
|
||||||
|
|
||||||
|
工作台对前端只暴露一套 SSE 协议,`cli` 与 `dify` 两条后端产出完全一致,**前端无需感知差异**:
|
||||||
|
|
||||||
|
| 事件 | 含义 |
|
||||||
|
|:--|:--|
|
||||||
|
| `{"delta": "..."}` | 文本增量,直接追加渲染 |
|
||||||
|
| `{"replace": "..."}` | 整体覆盖(终态校准) |
|
||||||
|
| `{"error": "..."}` | 错误(附可操作中文提示) |
|
||||||
|
| `data: [DONE]` | 流结束 |
|
||||||
|
|
||||||
|
CLI 侧事件解析(`--output-format stream-json --include-partial-messages`):
|
||||||
|
|
||||||
|
| CLI 事件 | 处理 |
|
||||||
|
|:--|:--|
|
||||||
|
| `type:"system", subtype:"init"` | 取 `session_id` |
|
||||||
|
| `stream_event` → `content_block_delta` → `delta.type==="text_delta"` | 取 `delta.text` 作为增量 |
|
||||||
|
| `stream_event` → `content_block_start`(tool_use) | 记录**工具调用边界**,见下 |
|
||||||
|
| `type:"assistant"` | 未产生增量流时的整段兜底 |
|
||||||
|
| `type:"result"` | 终态。`is_error:true` 时真实原因在 **`errors[]`**,正文作废 |
|
||||||
|
|
||||||
|
### 两个必须处理的文本污染
|
||||||
|
|
||||||
|
1. **报错被当成回答**:未认证时 CLI 把报错文本走文本通道发出来 →
|
||||||
|
`isCliBoilerplate()` 识别拦截,避免把英文报错显示成「AI 的回答」。
|
||||||
|
2. **工具调用前的过程话术**:模型发起搜索前常先吐一句
|
||||||
|
`I'll search for ... before responding.`,它不是答案。
|
||||||
|
→ `runCli` 记录每次 `tool_use` 的位置 `toolMarks`,**只保留最后一次工具调用之后的文本**;
|
||||||
|
由于这段已被流式推给前端,`server.js` 在成功分支比对 `r.text !== acc` 时补发 `{replace}` 整体覆盖。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 实测可用模型清单(本机 API Key 租户)
|
||||||
|
|
||||||
|
`--model` 的 `--help` 列表是**二进制里的静态清单,不等于已开通**。
|
||||||
|
下表是逐个真跑出来的结果:
|
||||||
|
|
||||||
|
| 模型 ID | 真实名称 | 实测 |
|
||||||
|
|:--|:--|:--|
|
||||||
|
| `deepseek-v4.1-flash` | DeepSeek-V4.1-Flash | ✅ **当前默认**(`clarify`) |
|
||||||
|
| `custom:custom-model-b5-standard` | GPT-5.5-Standard | ✅ 次选 |
|
||||||
|
| `custom:custom-model-b5-priority` | GPT-5.5-Priority | ✅ 同族「优先档」(资源紧张时优先路由,通常积分倍率更高) |
|
||||||
|
| `custom:custom-model-a4` | Claude-Opus-4.8 | ✅ |
|
||||||
|
| `custom:custom-model-b1-standard` / `-priority` | GPT-5.4-Standard / -Priority | ✅ |
|
||||||
|
| `custom:custom-model-b3-priority` | GPT-5.2-Priority | ✅ |
|
||||||
|
| `custom:custom-model-b4-standard` / `-priority` | GPT-5.1-Standard / -Priority | ✅ |
|
||||||
|
| `custom:custom-model-a1` | Claude-Opus-4.7 | ✅ |
|
||||||
|
| `custom:custom-model-a2` | Claude-Sonnet-4.6 | ✅ |
|
||||||
|
| `custom:custom-model-a3` | Claude-Opus-4.6 | ✅ |
|
||||||
|
| `custom:custom-model-c2-flash` | Gemini-3.5-Flash-1 | ✅ |
|
||||||
|
| `auto` / `hy3` / `glm-5.3` / `deepseek-v4-pro` | Auto / 混元3 / GLM-5.3 / DeepSeek-V4-Pro | ✅(deepseek 慢 3 倍) |
|
||||||
|
| `custom:doubao-seed-2-1-pro-260628` | — | ❌ **500**,未开通 |
|
||||||
|
| `custom:doubao-seed-2-0-lite-260428` | — | ❌ **400 `service info not found`** |
|
||||||
|
|
||||||
|
> **doubao 系在本机不可用**:既不在桌面产品配置的 48 个模型里,也不在该 API Key 租户的服务清单里。
|
||||||
|
> CLI 的静态清单里出现它,只是「名单上有、没开通」。
|
||||||
|
|
||||||
|
### 速度对比 A:短提示词(约 130 字输出,含 ~10s CLI 启动开销)
|
||||||
|
|
||||||
|
| 模型 | 耗时 |
|
||||||
|
|:--|:--|
|
||||||
|
| GPT-5.5-Standard | 15.7s |
|
||||||
|
| Claude-Opus-4.8 | 18.5s |
|
||||||
|
| Kimi-K2.5 | 21.7s |
|
||||||
|
| GLM-5.3 / GLM-5.3-Flash | 22.5s / 23.6s |
|
||||||
|
| **DeepSeek-V4-Pro** | **46.2s** 🐢 |
|
||||||
|
|
||||||
|
### 速度对比 B:端到端走 `/api/ai/clarify` 真实需求打磨(含账号设定 + 完整模板)
|
||||||
|
|
||||||
|
| 模型 | 普通创作问答 | 问热点(触发搜索) | `==OPTS==/==REQ==` 合规 |
|
||||||
|
|:--|:--|:--|:--|
|
||||||
|
| **GPT-5.5-Standard**(当前) | **42.2s** | 137.2s | ✅ |
|
||||||
|
| Claude-Opus-4.8 | 105.5s | 139.6s | ✅ |
|
||||||
|
| DeepSeek-V4-Pro | 119.5s | 151.2s | ✅ |
|
||||||
|
|
||||||
|
> 结论:**普通问答差距最大(2.5 倍)**;一旦触发联网搜索,各模型都要 130–150s,
|
||||||
|
> 瓶颈转移到搜索本身 → 这就是 `CLI_SEARCH_HINT` 要把搜索预算压到 ≤2 次的原因。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 排障速查
|
||||||
|
|
||||||
|
| 现象 | 原因 | 处理 |
|
||||||
|
|:--|:--|:--|
|
||||||
|
| 进程卡死无输出 | `SERVER__PORT` 未清除 → EADDRINUSE | 确认 §5.1 的 delete 生效 |
|
||||||
|
| `Cannot find module '../dist/codebuddy'` | bundle 分流未命中 | 设 `CODEBUDDY_FORCE_HEADLESS_BUNDLE=1` |
|
||||||
|
| `Authentication required. Please use /login` | 无凭据 | 按 §3 配 key |
|
||||||
|
| `ready:false` 但 key 已填 | `internetEnvironment` 版本不对 | 中国版必须是 `internal` |
|
||||||
|
| `500 internal server error` / `400 Custom model [...] service info not found` | 该模型未在当前 Key 租户开通 | 换 §9 表内 ✅ 的模型 |
|
||||||
|
| 首字要等 60s+ | 推理档位过高 | `"effort": "low"` |
|
||||||
|
| 回答里混进英文过程话术 | 工具调用前的叙述 | §8 的 `toolMarks` 切分(已内置) |
|
||||||
|
| HTTP 层全正常、agent 却不干活 | 典型 EADDRINUSE 假象 | 看 §5.1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 完整流程=两段,模型来源不同(⚠️ 容易搞混)
|
||||||
|
|
||||||
|
工作台「AI 创作」实际只有两个后端调用:
|
||||||
|
|
||||||
|
| 环节 | 接口 | 谁在跑 | 模型从哪来 |
|
||||||
|
|:--|:--|:--|:--|
|
||||||
|
| **需求打磨**(弹窗内对话) | `POST /api/ai/clarify` | **本方案**:本地 CLI 子进程 | `config.json → ai.cli.model` |
|
||||||
|
| **提交创作任务**(生成脚本 S1-S11) | `POST /api/run` | **WorkBuddy 宿主**:写 automation,由桌面端调度器执行 | **`sessions` 表里最近活跃的「手动会话」的 `model`**,兜底 `auto` |
|
||||||
|
|
||||||
|
> 🔴 **两者互不相干。** 改 `ai.cli.model` **不会**影响创作任务用的模型。
|
||||||
|
> 创作任务的模型是「跟随用户在 WorkBuddy UI 里最近选过的模型」——即浮动的,
|
||||||
|
> 从工作台侧看**不确定**。查证方法:
|
||||||
|
> ```sql
|
||||||
|
> SELECT model_id, COUNT(*) FROM automations GROUP BY model_id ORDER BY 2 DESC;
|
||||||
|
> ```
|
||||||
|
> 本机实测历史分布:`hy4-preview` 30 条 / `deepseek-v4.1-flash` 6 条。
|
||||||
|
> 想让创作任务固定用某个模型,需要在 UI 里把当前会话模型切到它(或改造 `/api/run` 支持显式 `model_id`)。
|
||||||
|
|
||||||
|
### 完整链路自检结果(2026-09-29,model = deepseek-v4.1-flash)
|
||||||
|
|
||||||
|
| 环节 | 结果 |
|
||||||
|
|:--|:--|
|
||||||
|
| `GET /api/ai/status` | backend=`cli` · model=`deepseek-v4.1-flash` · ready=**true** |
|
||||||
|
| `GET /api/ai/status?probe=1` | ok=**true** · 17.0s · 返回「成功」 |
|
||||||
|
| `POST /api/ai/clarify`(stream) | 371 字 · `==OPTS==/==REQ==` 合规 **✅** · 无英文残留 **✅** · 51.7s |
|
||||||
|
| `POST /api/run` → 轮询状态 | queued(5s) → running(30s) → **done**(50s) · `model_id` 落库 = `deepseek-v4.1-flash` **✅** |
|
||||||
|
|
||||||
|
> 注:`/api/ai/topics` 是**遗留死代码** —— 前端从不调用它(选题列表走 `/api/dsh/topics`)。
|
||||||
|
> 它仍写死 Dify,未配 `DIFY_MCN_CYLG_KEY` 时会 500,但**不影响真实流程**。
|
||||||
File renamed without changes.
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# 短视频工作台 · 视觉与交互规范(最终态)
|
# 短视频工作台 · 视觉与交互规范(最终态)
|
||||||
|
|
||||||
> **定位**:本文件是工作台前端(`mcn-work-shop/`)视觉与交互的**唯一权威**——设计 token、布局框架、**逐页最终态**、组件规格、交互链路、已知坑。
|
> **定位**:本文件是工作台前端(`mcn-workshop/`)视觉与交互的**唯一权威**——设计 token、布局框架、**逐页最终态**、组件规格、交互链路、已知坑。
|
||||||
> **用法**:新增页面 / 改样式 / 改交互前先读本文件;改完**回写本文件**(含文末变更记录),避免同类坑重复踩。
|
> **用法**:新增页面 / 改样式 / 改交互前先读本文件;改完**回写本文件**(含文末变更记录),避免同类坑重复踩。
|
||||||
> **技术基线**:零依赖 Node 服务 + 原生 JS(无框架无构建);`public/` 下 `index.html` / `style.css` / `app.js` / `data-pages.js` / `help-guide.js`。
|
> **技术基线**:零依赖 Node 服务 + 原生 JS(无框架无构建);`public/` 下 `index.html` / `style.css` / `app.js` / `data-pages.js` / `help-guide.js`。
|
||||||
> **最后更新**:2026-09-16(本轮把此前的 `工作台UI规范.md` 合并升级为本文件并重命名为「视觉交互规范」,内容为**迭代后的最终态**)
|
> **最后更新**:2026-09-16(本轮把此前的 `工作台UI规范.md` 合并升级为本文件并重命名为「视觉交互规范」,内容为**迭代后的最终态**)
|
||||||
File renamed without changes.
File renamed without changes.
+18
-2
@@ -11,6 +11,10 @@ const DEFAULTS = {
|
|||||||
outputRoot: 'MCNSkill项目',
|
outputRoot: 'MCNSkill项目',
|
||||||
businessDataDir: ['MCNSkill项目', '三方数据', '红狐数据', '抖音榜单'],
|
businessDataDir: ['MCNSkill项目', '三方数据', '红狐数据', '抖音榜单'],
|
||||||
extraRoots: [],
|
extraRoots: [],
|
||||||
|
// 10-08:工作台提交的 AI 任务会话归属哪个工作区 —— 侧边栏分组按会话 cwd 聚合,
|
||||||
|
// cwd 不是已打开过的工作区就会落到「未分组任务」。
|
||||||
|
// 留空 → 用工作台自身目录(ROOT);填绝对路径 → 会话归到那个工作区(改分组只改这里,不动代码)。
|
||||||
|
sessionCwd: '',
|
||||||
// 09-29 AI 通路配置:backend 决定走哪条链路
|
// 09-29 AI 通路配置:backend 决定走哪条链路
|
||||||
// "cli" 本地 CodeBuddy CLI 进程(stdio,无端口;需自备 CODEBUDDY_API_KEY)
|
// "cli" 本地 CodeBuddy CLI 进程(stdio,无端口;需自备 CODEBUDDY_API_KEY)
|
||||||
// "dify" Dify chat-messages(原链路,保留兜底)
|
// "dify" Dify chat-messages(原链路,保留兜底)
|
||||||
@@ -19,11 +23,23 @@ const DEFAULTS = {
|
|||||||
cli: {
|
cli: {
|
||||||
path: '', // 留空 → 自动探测(环境变量 CODEBUDDY_CLI_PATH > WorkBuddy 自带 CLI > PATH)
|
path: '', // 留空 → 自动探测(环境变量 CODEBUDDY_CLI_PATH > WorkBuddy 自带 CLI > PATH)
|
||||||
model: 'custom:doubao-seed-2-1-pro-260628',
|
model: 'custom:doubao-seed-2-1-pro-260628',
|
||||||
apiKey: '', // 留空 → 读环境变量 CODEBUDDY_API_KEY
|
apiKey: '', // 一般留空:config.json 是 git 跟踪文件,key 别写这里
|
||||||
authToken: '', // 留空 → 读环境变量 CODEBUDDY_AUTH_TOKEN(优先级最高)
|
authToken: '', // 同上
|
||||||
|
// 凭据文件(JSON:{"apiKey":"..."} 或 {"authToken":"..."})。
|
||||||
|
// 留空 → 默认 ~/.workbuddy/mcn-workshop/ai-cli.json(刻意放仓库外,不会被技能同步复制)
|
||||||
|
credentialFile: '',
|
||||||
internetEnvironment: 'internal', // 中国版=internal,iOA=ioa,国际版留空
|
internetEnvironment: 'internal', // 中国版=internal,iOA=ioa,国际版留空
|
||||||
permissionMode: '',
|
permissionMode: '',
|
||||||
timeoutMs: 300000,
|
timeoutMs: 300000,
|
||||||
|
// 推理开销档位:minimal|low|medium|high|xhigh|max(留空=模型默认)。
|
||||||
|
// 这是首字延迟的第一大杠杆:reasoning 模型默认档位会让首字等到 60s+。
|
||||||
|
effort: 'low',
|
||||||
|
// 联网搜索:true → 工具面收敛为 WebSearch+WebFetch(启动快,且模型碰不到文件系统)
|
||||||
|
search: true,
|
||||||
|
tools: '', // 显式覆盖工具白名单:'' = 按 search 推导;'none' = 全禁
|
||||||
|
strictMcp: true, // 屏蔽宿主 MCP(weixinpay/sheetagent 等)
|
||||||
|
appendSystemPrompt: '', // 追加系统提示
|
||||||
|
maxTurns: 6, // agent 轮数硬上限(防搜索/工具调用失控跑飞)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+58
-2
@@ -70,7 +70,8 @@ async function executeTask(opts) {
|
|||||||
});
|
});
|
||||||
if (d.error) throw new Error(d.error);
|
if (d.error) throw new Error(d.error);
|
||||||
taskId = d.id;
|
taskId = d.id;
|
||||||
showToast('✅ 任务已提交,请在左侧会话栏查看执行');
|
showToast('✅ 任务已提交,执行进度见右下角面板');
|
||||||
|
TaskProgress.show(opts.label || '工作台任务');
|
||||||
if (opts.onSubmit) { try { opts.onSubmit(taskId); } catch (e) {} }
|
if (opts.onSubmit) { try { opts.onSubmit(taskId); } catch (e) {} }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('❌ 任务提交失败: ' + e.message);
|
showToast('❌ 任务提交失败: ' + e.message);
|
||||||
@@ -82,6 +83,7 @@ async function executeTask(opts) {
|
|||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
await new Promise((r) => setTimeout(r, 5000));
|
await new Promise((r) => setTimeout(r, 5000));
|
||||||
try { s = await api('/api/run/status?id=' + encodeURIComponent(taskId)); } catch (e) { continue; }
|
try { s = await api('/api/run/status?id=' + encodeURIComponent(taskId)); } catch (e) { continue; }
|
||||||
|
if (s) TaskProgress.update(s, opts.label || '工作台任务');
|
||||||
if (!s || s.state === 'notfound') { finished = true; finish(s || null); break; }
|
if (!s || s.state === 'notfound') { finished = true; finish(s || null); break; }
|
||||||
if (s.state === 'done') { finished = true; finish(s); showToast('✅ 任务执行完成,页面已自动刷新'); refreshCurrentPage(); break; }
|
if (s.state === 'done') { finished = true; finish(s); showToast('✅ 任务执行完成,页面已自动刷新'); refreshCurrentPage(); break; }
|
||||||
if (s.state === 'error') { finished = true; finish(s); showToast('❌ 任务执行失败' + (s.error ? ':' + s.error : '')); break; }
|
if (s.state === 'error') { finished = true; finish(s); showToast('❌ 任务执行失败' + (s.error ? ':' + s.error : '')); break; }
|
||||||
@@ -90,7 +92,9 @@ async function executeTask(opts) {
|
|||||||
}
|
}
|
||||||
restore();
|
restore();
|
||||||
if (!ended) finish(null);
|
if (!ended) finish(null);
|
||||||
if (!finished) showToast('⏳ 任务仍在执行,请到左侧会话栏查看进度');
|
if (!finished) showToast('⏳ 任务仍在执行,右下角面板可继续观察进度');
|
||||||
|
// 终态保留 8 秒展示结果摘要(状态/工具调用/最近动作)后自动收起;未完成则保留面板供继续观察
|
||||||
|
if (finished) setTimeout(() => TaskProgress.hide(), 8000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通用确认弹窗:确认后执行 AI 任务(opts: title/desc/target/url/prompt/label/btn/afterText)
|
// 通用确认弹窗:确认后执行 AI 任务(opts: title/desc/target/url/prompt/label/btn/afterText)
|
||||||
@@ -263,6 +267,58 @@ function showToast(msg) {
|
|||||||
_toastTimer = setTimeout(() => t.classList.remove('show'), 1800);
|
_toastTimer = setTimeout(() => t.classList.remove('show'), 1800);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================= 任务进度面板(10-08 新增) =================
|
||||||
|
后台任务原本只弹一句「已提交」,用户无法判断到底在跑还是卡住、做了什么。
|
||||||
|
本面板把 /api/run/status 的进度(状态/耗时/工具调用/最近动作/是否停滞)持续显示,
|
||||||
|
不必再去左侧会话栏翻找。用法:TaskProgress.show(标题) → 轮询中 TaskProgress.update(s) → TaskProgress.hide() */
|
||||||
|
const TaskProgress = (function () {
|
||||||
|
let el = null;
|
||||||
|
const fmtDur = (ms) => {
|
||||||
|
if (ms == null) return '—';
|
||||||
|
const s = Math.max(0, Math.round(Number(ms) / 1000));
|
||||||
|
if (s < 60) return s + ' 秒';
|
||||||
|
return Math.floor(s / 60) + ' 分 ' + (s % 60) + ' 秒';
|
||||||
|
};
|
||||||
|
const STATE_TXT = { queued: '排队中', pending: '等待调度', running: '执行中', done: '已完成', error: '执行失败', review: '待确认', notfound: '任务已清理' };
|
||||||
|
function ensure() {
|
||||||
|
if (el && document.body.contains(el)) return el;
|
||||||
|
el = document.createElement('div');
|
||||||
|
el.className = 'tp-panel';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
function render(title, s) {
|
||||||
|
const box = ensure();
|
||||||
|
const st = (s && s.state) || 'queued';
|
||||||
|
const dotCls = st === 'running' ? 'run' : (st === 'done' ? 'done' : (st === 'error' ? 'err' : ''));
|
||||||
|
const tc = (s && s.toolCalls) ? s.toolCalls : {};
|
||||||
|
const tools = Object.keys(tc).map((k) => '<span>' + esc(k) + ' ×' + tc[k] + '</span>').join('');
|
||||||
|
const stalled = st === 'running' && !!(s && s.stalled);
|
||||||
|
const ago = (s && s.updatedAgoMs != null) ? fmtDur(s.updatedAgoMs) : null;
|
||||||
|
box.innerHTML =
|
||||||
|
'<div class="tp-head"><i class="tp-dot ' + dotCls + '"></i>'
|
||||||
|
+ '<div class="tp-title">' + esc(title || (s && s.sessionTitle) || 'AI 任务') + '</div>'
|
||||||
|
+ '<button class="tp-close" title="关闭">×</button></div>'
|
||||||
|
+ '<div class="tp-body">'
|
||||||
|
+ '<div class="tp-row"><span class="k">状态</span><span class="v">' + esc(STATE_TXT[st] || st)
|
||||||
|
+ (s && s.elapsedMs != null ? '(已运行 ' + fmtDur(s.elapsedMs) + ')' : '') + '</span></div>'
|
||||||
|
+ (tools ? '<div class="tp-row"><span class="k">工具</span><span class="v"><div class="tp-tools">' + tools + '</div></span></div>' : '')
|
||||||
|
+ (s && s.lastAction ? '<div class="tp-row"><span class="k">最近动作</span><span class="v"></span></div><div class="tp-act">' + esc(s.lastAction) + '</div>' : '')
|
||||||
|
+ (ago && st === 'running' ? '<div class="tp-row" style="margin-top:6px"><span class="k">活跃</span><span class="v">'
|
||||||
|
+ (stalled ? '已 ' : '') + esc(ago) + '前有输出</span></div>' : '')
|
||||||
|
+ (stalled ? '<div class="tp-warn">⚠️ 超过 90 秒没有新输出,可能卡住或等待中</div>' : '')
|
||||||
|
+ (s && s.error ? '<div class="tp-warn">错误:' + esc(String(s.error)) + '</div>' : '')
|
||||||
|
+ '</div>';
|
||||||
|
const btn = box.querySelector('.tp-close');
|
||||||
|
if (btn) btn.onclick = hide;
|
||||||
|
}
|
||||||
|
function show(title, s) { render(title, s || null); }
|
||||||
|
function update(s, title) { render(title, s); }
|
||||||
|
function hide() { if (el && el.parentNode) el.parentNode.removeChild(el); el = null; }
|
||||||
|
return { show, update, hide };
|
||||||
|
})();
|
||||||
|
window.TaskProgress = TaskProgress;
|
||||||
|
|
||||||
/* ================= 迷你 Markdown 渲染器 ================= */
|
/* ================= 迷你 Markdown 渲染器 ================= */
|
||||||
function inlineMd(s) {
|
function inlineMd(s) {
|
||||||
let out = esc(s);
|
let out = esc(s);
|
||||||
+22
-2
@@ -1056,19 +1056,32 @@ async function updateRankingData(view, btn) {
|
|||||||
body: JSON.stringify({ prompt: rankPrompt, force: !!inCooldown }),
|
body: JSON.stringify({ prompt: rankPrompt, force: !!inCooldown }),
|
||||||
});
|
});
|
||||||
if (!r || r.ok === false) throw new Error((r && r.error) || '任务提交失败');
|
if (!r || r.ok === false) throw new Error((r && r.error) || '任务提交失败');
|
||||||
showToast('✅ 榜单更新任务已提交,请到左侧会话栏查看执行');
|
showToast('✅ 榜单更新任务已提交,执行进度见右下角面板');
|
||||||
|
TaskProgress.show('更新榜单数据');
|
||||||
// 4) 轮询任务状态(最长 15 分钟;期间按钮保持 busy)
|
// 4) 轮询任务状态(最长 15 分钟;期间按钮保持 busy)
|
||||||
|
// 10-08 修复:工作台服务被回收时 api() 会持续抛错,原逻辑静默 continue 直到 15 分钟超时、用户零感知
|
||||||
|
// ⇒ 连续 3 次查询失败即判定服务已断开,提示并结束轮询(任务仍在后台跑,只是工作台看不到进度)
|
||||||
const deadline = Date.now() + 15 * 60 * 1000;
|
const deadline = Date.now() + 15 * 60 * 1000;
|
||||||
let state = 'running';
|
let state = 'running';
|
||||||
|
let pollFail = 0;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
await new Promise((res) => setTimeout(res, 5000));
|
await new Promise((res) => setTimeout(res, 5000));
|
||||||
let s = null;
|
let s = null;
|
||||||
try { s = await api('/api/run/status?id=' + encodeURIComponent(r.id)); } catch (e) { continue; }
|
try { s = await api('/api/run/status?id=' + encodeURIComponent(r.id)); pollFail = 0; if (s) TaskProgress.update(s, '更新榜单数据'); }
|
||||||
|
catch (e) {
|
||||||
|
if (++pollFail >= 3) {
|
||||||
|
restore();
|
||||||
|
showToast('⚠️ 工作台服务已断开(连续 3 次查询失败);任务仍在后台执行,重启工作台后请到左侧会话栏查看进度');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!s || s.state === 'notfound') { state = 'done'; break; }
|
if (!s || s.state === 'notfound') { state = 'done'; break; }
|
||||||
if (s.state === 'done') { state = 'done'; break; }
|
if (s.state === 'done') { state = 'done'; break; }
|
||||||
if (s.state === 'error' || s.state === 'review') { state = s.state; break; }
|
if (s.state === 'error' || s.state === 'review') { state = s.state; break; }
|
||||||
}
|
}
|
||||||
restore();
|
restore();
|
||||||
|
if (state !== 'running') setTimeout(() => TaskProgress.hide(), 8000);
|
||||||
if (state === 'done') {
|
if (state === 'done') {
|
||||||
showToast('✅ 榜单数据已更新,正在刷新…');
|
showToast('✅ 榜单数据已更新,正在刷新…');
|
||||||
// 5) 整页重建:丢弃 _reload(同闭包 render 不清 meta 缓存)→ router 重新进入渲染 → 拉到最新数据期
|
// 5) 整页重建:丢弃 _reload(同闭包 render 不清 meta 缓存)→ router 重新进入渲染 → 拉到最新数据期
|
||||||
@@ -1216,12 +1229,15 @@ function updateFollowedAccounts(btn) {
|
|||||||
});
|
});
|
||||||
if (r.error) throw new Error(r.error);
|
if (r.error) throw new Error(r.error);
|
||||||
const taskId = r.id;
|
const taskId = r.id;
|
||||||
|
const taskTitle = '更新账号·' + name;
|
||||||
|
TaskProgress.show(taskTitle);
|
||||||
const deadline = Date.now() + 20 * 60 * 1000;
|
const deadline = Date.now() + 20 * 60 * 1000;
|
||||||
let state = 'running';
|
let state = 'running';
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
await new Promise((res) => setTimeout(res, 5000));
|
await new Promise((res) => setTimeout(res, 5000));
|
||||||
let s = null;
|
let s = null;
|
||||||
try { s = await api('/api/run/status?id=' + encodeURIComponent(taskId)); } catch (e) { continue; }
|
try { s = await api('/api/run/status?id=' + encodeURIComponent(taskId)); } catch (e) { continue; }
|
||||||
|
if (s) TaskProgress.update(s, taskTitle);
|
||||||
if (!s || s.state === 'notfound') { state = 'done'; break; }
|
if (!s || s.state === 'notfound') { state = 'done'; break; }
|
||||||
if (s.state === 'done') { state = 'done'; break; }
|
if (s.state === 'done') { state = 'done'; break; }
|
||||||
if (s.state === 'error' || s.state === 'review') { state = s.state; break; }
|
if (s.state === 'error' || s.state === 'review') { state = s.state; break; }
|
||||||
@@ -1231,6 +1247,7 @@ function updateFollowedAccounts(btn) {
|
|||||||
} catch (e) { failed.push(name + '(' + e.message + ')'); }
|
} catch (e) { failed.push(name + '(' + e.message + ')'); }
|
||||||
}
|
}
|
||||||
restore();
|
restore();
|
||||||
|
TaskProgress.hide();
|
||||||
if (btn) btn.blur();
|
if (btn) btn.blur();
|
||||||
showToast(done.length ? `✅ 已更新 ${done.length} 个关注账号${failed.length ? `;${failed.length} 个未完成:${failed.join('、')}` : ''}` : '❌ 更新未完成:' + failed.join('、'));
|
showToast(done.length ? `✅ 已更新 ${done.length} 个关注账号${failed.length ? `;${failed.length} 个未完成:${failed.join('、')}` : ''}` : '❌ 更新未完成:' + failed.join('、'));
|
||||||
refreshCurrentPage();
|
refreshCurrentPage();
|
||||||
@@ -1950,12 +1967,15 @@ function readVideoTaskRec(videoId) {
|
|||||||
/* 刷新后恢复轮询:跟随该视频解析任务直到终态 → 清理记录 → 提示并刷新本页按钮态 */
|
/* 刷新后恢复轮询:跟随该视频解析任务直到终态 → 清理记录 → 提示并刷新本页按钮态 */
|
||||||
async function watchVideoParseTask(videoId, taskId, reload) {
|
async function watchVideoParseTask(videoId, taskId, reload) {
|
||||||
let s = null;
|
let s = null;
|
||||||
|
TaskProgress.show('视频解析');
|
||||||
const deadline = Date.now() + 35 * 60 * 1000;
|
const deadline = Date.now() + 35 * 60 * 1000;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
await new Promise((r) => setTimeout(r, 5000));
|
await new Promise((r) => setTimeout(r, 5000));
|
||||||
try { s = await api('/api/run/status?id=' + encodeURIComponent(taskId)); } catch (e) { continue; }
|
try { s = await api('/api/run/status?id=' + encodeURIComponent(taskId)); } catch (e) { continue; }
|
||||||
|
if (s) TaskProgress.update(s, '视频解析');
|
||||||
if (!s || s.state === 'notfound' || s.state === 'done' || s.state === 'error' || s.state === 'review') break;
|
if (!s || s.state === 'notfound' || s.state === 'done' || s.state === 'error' || s.state === 'review') break;
|
||||||
}
|
}
|
||||||
|
setTimeout(() => TaskProgress.hide(), 8000);
|
||||||
try { localStorage.removeItem(VIDEO_TASK_KEY(videoId)); } catch (e) {}
|
try { localStorage.removeItem(VIDEO_TASK_KEY(videoId)); } catch (e) {}
|
||||||
if (s && s.state === 'done') showToast('✅ 视频解析完成,已自动刷新本页');
|
if (s && s.state === 'done') showToast('✅ 视频解析完成,已自动刷新本页');
|
||||||
else if (s && s.state === 'error') showToast('❌ 视频解析失败' + (s.error ? ':' + s.error : '') + '(任务已结束,可重新点击解析)');
|
else if (s && s.state === 'error') showToast('❌ 视频解析失败' + (s.error ? ':' + s.error : '') + '(任务已结束,可重新点击解析)');
|
||||||
File renamed without changes.
File renamed without changes.
+25
@@ -880,3 +880,28 @@ td.td-title { max-width: 240px; }
|
|||||||
.g-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
|
.g-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
|
||||||
.g-actions .btn-sm { padding: 4px 12px; font-size: 14px; }
|
.g-actions .btn-sm { padding: 4px 12px; font-size: 14px; }
|
||||||
.g-actions .ghost:disabled { opacity: .5; cursor: default; }
|
.g-actions .ghost:disabled { opacity: .5; cursor: default; }
|
||||||
|
|
||||||
|
/* ===== 任务进度面板(10-08 新增:后台 AI 任务执行过程可见) ===== */
|
||||||
|
.tp-panel {
|
||||||
|
position: fixed; right: 18px; bottom: 18px; z-index: 9999;
|
||||||
|
width: 380px; max-width: calc(100vw - 36px);
|
||||||
|
background: var(--panel); border: 1px solid var(--border); border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 28px rgba(0,0,0,.16); overflow: hidden;
|
||||||
|
font-size: 13.5px; color: var(--text);
|
||||||
|
}
|
||||||
|
.tp-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--border); background: var(--primary-soft); }
|
||||||
|
.tp-title { font-weight: 700; font-size: 14px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.tp-close { border: none; background: transparent; color: var(--dim); cursor: pointer; font-size: 16px; line-height: 1; padding: 2px 4px; }
|
||||||
|
.tp-body { padding: 10px 12px; }
|
||||||
|
.tp-row { display: flex; gap: 8px; margin-bottom: 6px; }
|
||||||
|
.tp-row .k { color: var(--dim); flex: 0 0 62px; }
|
||||||
|
.tp-row .v { flex: 1; word-break: break-all; }
|
||||||
|
.tp-tools { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
|
.tp-tools span { background: #f0f2f5; border-radius: 20px; padding: 2px 9px; font-size: 12px; font-variant-numeric: tabular-nums; }
|
||||||
|
.tp-act { max-height: 92px; overflow: auto; background: #fafbfc; border: 1px solid var(--border); border-radius: 8px; padding: 8px 9px; color: var(--dim); font-size: 12.5px; line-height: 1.55; white-space: pre-wrap; word-break: break-word; }
|
||||||
|
.tp-warn { margin-top: 8px; padding: 7px 9px; border-radius: 8px; background: #fff3e6; color: #b3541e; font-size: 12.5px; font-weight: 600; }
|
||||||
|
.tp-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--primary); flex: 0 0 8px; }
|
||||||
|
.tp-dot.run { animation: tp-pulse 1.1s ease-in-out infinite; }
|
||||||
|
.tp-dot.done { background: #2f9e44; }
|
||||||
|
.tp-dot.err { background: var(--danger); }
|
||||||
|
@keyframes tp-pulse { 0%,100% { opacity: 1; } 50% { opacity: .25; } }
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+128
-40
@@ -13,7 +13,7 @@ const dsh = require('./dsh-data');
|
|||||||
const { parseAttachment } = require('./scripts/attachment-parser');
|
const { parseAttachment } = require('./scripts/attachment-parser');
|
||||||
const { loadConfig } = require('./load-config');
|
const { loadConfig } = require('./load-config');
|
||||||
// 09-29 新增:本地 CodeBuddy CLI 后端(stdio 直连,替代 Dify;见 cli-backend.js 头部说明)
|
// 09-29 新增:本地 CodeBuddy CLI 后端(stdio 直连,替代 Dify;见 cli-backend.js 头部说明)
|
||||||
const { runCli, resolveCli } = require('./cli-backend');
|
const { runCli, resolveCli, resolveCredentials } = require('./cli-backend');
|
||||||
|
|
||||||
// 09-01 默认端口由 8899 改为 8900:本机 8899 被小米 PC 管家 MiPCAudio.exe 系统服务占用(0.0.0.0 监听且自动复活),
|
// 09-01 默认端口由 8899 改为 8900:本机 8899 被小米 PC 管家 MiPCAudio.exe 系统服务占用(0.0.0.0 监听且自动复活),
|
||||||
// 此时 127.0.0.1:8899 绑定不生效 → 默认直接起在 8900;仍可 `node server.js <端口>` 显式换端口
|
// 此时 127.0.0.1:8899 绑定不生效 → 默认直接起在 8900;仍可 `node server.js <端口>` 显式换端口
|
||||||
@@ -34,10 +34,46 @@ function aiBackend() {
|
|||||||
return String((CFG.ai && CFG.ai.backend) || 'dify').toLowerCase();
|
return String((CFG.ai && CFG.ai.backend) || 'dify').toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 09-29 CLI 后端的联网搜索策略(追加到系统提示)。
|
||||||
|
// 根因:CLI 自带 WebSearch/WebFetch,但默认不会主动用;而每次都搜又会把首字延迟从十几秒拖到几十秒。
|
||||||
|
// 所以给模型一条明确的「什么时候才搜」的判据,而不是简单开关。
|
||||||
|
const CLI_SEARCH_HINT =
|
||||||
|
'你具备联网搜索能力(WebSearch / WebFetch),但搜索很慢,要严格按判据使用。'
|
||||||
|
+ '【该搜】回答依赖「今天/近期」的外部事实时:当下热点、平台规则与趋势、行业数据、事实核查。'
|
||||||
|
+ '【不该搜】创作讨论、需求澄清、脚本撰写、文案打磨——这些靠你的知识直接答,一律不搜。'
|
||||||
|
+ '【搜索预算】中文关键词;最多 1 次 WebSearch;仅当搜索结果指向必须打开的页面时才追加 1 次 WebFetch;'
|
||||||
|
+ '工具调用总数硬上限 2 次。搜到即答,禁止反复搜或换词重搜。'
|
||||||
|
+ '搜索失败或超时就直接凭已有信息作答,不要重试。';
|
||||||
|
|
||||||
|
// 由 config.json 的 ai.cli 段构造一次 CLI 调用参数(clarify / 自检 / 后续其它入口共用,避免多处硬编码)
|
||||||
|
function cliBaseOpts(over) {
|
||||||
|
const c = (CFG.ai && CFG.ai.cli) || {};
|
||||||
|
const searchOn = c.search !== false;
|
||||||
|
return Object.assign({
|
||||||
|
cwd: ROOT,
|
||||||
|
// MCN_AI_MODEL 可临时覆盖模型(用于不重启、不改配置的 A/B 对比与排障)
|
||||||
|
model: String(process.env.MCN_AI_MODEL || '').trim() || c.model,
|
||||||
|
apiKey: c.apiKey,
|
||||||
|
authToken: c.authToken,
|
||||||
|
credentialFile: c.credentialFile,
|
||||||
|
internetEnvironment: c.internetEnvironment,
|
||||||
|
permissionMode: c.permissionMode,
|
||||||
|
cliPath: c.path,
|
||||||
|
timeoutMs: Math.max(30000, parseInt(c.timeoutMs, 10) || 300000),
|
||||||
|
effort: c.effort || '',
|
||||||
|
search: searchOn,
|
||||||
|
tools: c.tools,
|
||||||
|
strictMcp: c.strictMcp !== false,
|
||||||
|
maxTurns: c.maxTurns,
|
||||||
|
appendSystemPrompt: c.appendSystemPrompt || (searchOn ? CLI_SEARCH_HINT : ''),
|
||||||
|
}, over || {});
|
||||||
|
}
|
||||||
|
|
||||||
// 09-10 路径治本(配套「仓库特征判定」):工作台运行时路径一律从 ROOT 动态推导,禁止写死盘符。
|
// 09-10 路径治本(配套「仓库特征判定」):工作台运行时路径一律从 ROOT 动态推导,禁止写死盘符。
|
||||||
// 根因:原写死盘符指向仓库外的固定目录,仓库迁移后该目录不存在 → 浏览器锁失效、会话 cwd 悬空。
|
// 根因:原写死盘符指向仓库外的固定目录,仓库迁移后该目录不存在 → 浏览器锁失效、会话 cwd 悬空。
|
||||||
const BROWSER_LOCK_FILE = path.join(ROOT, '.browser-lock'); // 浏览器互斥锁文件(与工作台同目录)
|
const BROWSER_LOCK_FILE = path.join(ROOT, '.browser-lock'); // 浏览器互斥锁文件(与工作台同目录)
|
||||||
const SESSION_CWD = ROOT; // 工作台触发的 AI 会话 cwd(分组名 = mcn-work-shop)
|
// 10-08:默认工作台自身目录;config.json 的 sessionCwd 可指定别的工作区(分组随之改变)
|
||||||
|
const SESSION_CWD = String(CFG.sessionCwd || '').trim() || ROOT;
|
||||||
|
|
||||||
// 09-07:prompt 模板文件化(prompts/*.md)——规则迭代只改文件不碰代码;文件缺失/损坏回退 null,调用方自行兜底。
|
// 09-07:prompt 模板文件化(prompts/*.md)——规则迭代只改文件不碰代码;文件缺失/损坏回退 null,调用方自行兜底。
|
||||||
// 模板正文取 ==PROMPT== 与 ==CHANGELOG== 之间(头部说明/文末变更记录不发给模型)
|
// 模板正文取 ==PROMPT== 与 ==CHANGELOG== 之间(头部说明/文末变更记录不发给模型)
|
||||||
@@ -359,7 +395,7 @@ function createOnceAutomation(name, prompt, skillsArr, connectorIdsArr) {
|
|||||||
try {
|
try {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const id = 'automation-' + now;
|
const id = 'automation-' + now;
|
||||||
const cwd = SESSION_CWD; // 工作台触发的会话归入 mcn-work-shop 空间分组(由 ROOT 动态推导)
|
const cwd = SESSION_CWD; // 工作台触发的会话归入 mcn-workshop 空间分组(由 ROOT 动态推导)
|
||||||
const d = new Date(now + 5 * 1000);
|
const d = new Date(now + 5 * 1000);
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
const scheduledAt = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
const scheduledAt = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||||
@@ -401,6 +437,55 @@ function rankLastTaskState() {
|
|||||||
} catch (e) { return null; }
|
} catch (e) { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 任务进度可见(10-08 新增:用户「我需要看到 才知道执行的是否正常」)----------
|
||||||
|
// 后台任务只返回 running/done 太粗:用户无法判断"在真跑"还是"卡住了"。
|
||||||
|
// 解法:automation → 关联它拉起的后台会话 → 读该会话 jsonl 尾部 → 产出
|
||||||
|
// 已运行时长 / 工具调用统计 / 最近一段动作 / 会话最后写入距今(判断是否停滞)
|
||||||
|
|
||||||
|
// 会话 cwd → WorkBuddy projects 目录名(实测:`D:\a\b\V1.0\mcn-workshop` → `d-a-b-V1.0-mcn-workshop`)
|
||||||
|
function escapeCwdForProjects(cwd) {
|
||||||
|
const s = String(cwd || '').replace(/\\/g, '/');
|
||||||
|
const m = s.match(/^([A-Za-z]):\/(.+)$/);
|
||||||
|
if (!m) return null;
|
||||||
|
return m[1].toLowerCase() + '-' + m[2].replace(/\//g, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOOL_NAME_RE = /"name":"(Bash|Read|Write|Edit|Grep|Glob|WebSearch|WebFetch|WebFetch2|Skill|Task|Agent|TodoWrite|ExitPlanMode|mcp__[A-Za-z0-9_]+)"/g;
|
||||||
|
|
||||||
|
function readSessionProgress(sessionId, cwd) {
|
||||||
|
const out = { lastAction: '', toolCalls: {}, updatedAgoMs: null, stalled: false, logBytes: 0 };
|
||||||
|
try {
|
||||||
|
const esc = escapeCwdForProjects(cwd);
|
||||||
|
if (!esc) return out;
|
||||||
|
const f = path.join(os.homedir(), '.workbuddy', 'projects', esc, sessionId + '.jsonl');
|
||||||
|
if (!fs.existsSync(f)) return out;
|
||||||
|
const st = fs.statSync(f);
|
||||||
|
out.logBytes = st.size;
|
||||||
|
out.updatedAgoMs = Date.now() - Number(st.mtimeMs);
|
||||||
|
// 90 秒没再写入即视为停滞(模型正常推进时几乎每秒都在写日志)
|
||||||
|
out.stalled = out.updatedAgoMs > 90000;
|
||||||
|
if (st.size <= 0) return out;
|
||||||
|
const readLen = Math.min(st.size, 256 * 1024);
|
||||||
|
const fd = fs.openSync(f, 'r');
|
||||||
|
const buf = Buffer.alloc(readLen);
|
||||||
|
fs.readSync(fd, buf, 0, readLen, st.size - readLen);
|
||||||
|
fs.closeSync(fd);
|
||||||
|
const txt = buf.toString('utf8');
|
||||||
|
let m;
|
||||||
|
TOOL_NAME_RE.lastIndex = 0;
|
||||||
|
while ((m = TOOL_NAME_RE.exec(txt))) out.toolCalls[m[1]] = (out.toolCalls[m[1]] || 0) + 1;
|
||||||
|
const texts = [];
|
||||||
|
const re2 = /"text":"((?:[^"\\]|\\.){4,})"/g;
|
||||||
|
while ((m = re2.exec(txt))) {
|
||||||
|
let s = m[1];
|
||||||
|
try { s = JSON.parse('"' + s + '"'); } catch (e) { /* 解析失败保留原串 */ }
|
||||||
|
if (s && String(s).trim()) texts.push(String(s).trim());
|
||||||
|
}
|
||||||
|
if (texts.length) out.lastAction = texts[texts.length - 1].replace(/\s+/g, ' ').slice(0, 160);
|
||||||
|
} catch (e) { /* 读不到不影响状态判定 */ }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 静态文件 / API ----
|
// ---- 静态文件 / API ----
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
const u = new URL(req.url, 'http://localhost');
|
const u = new URL(req.url, 'http://localhost');
|
||||||
@@ -777,7 +862,7 @@ const server = http.createServer((req, res) => {
|
|||||||
const db = new DatabaseSync(process.env.WORKBUDDY_DB || path.join(os.homedir(), '.workbuddy', 'workbuddy.db'));
|
const db = new DatabaseSync(process.env.WORKBUDDY_DB || path.join(os.homedir(), '.workbuddy', 'workbuddy.db'));
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const id = 'automation-' + now;
|
const id = 'automation-' + now;
|
||||||
const cwd = SESSION_CWD; // 工作台触发的会话归入 mcn-work-shop 空间分组(由 ROOT 动态推导)
|
const cwd = SESSION_CWD; // 工作台触发的会话归入 mcn-workshop 空间分组(由 ROOT 动态推导)
|
||||||
const d = new Date(Date.now() + 5 * 1000); // 未来 5 秒(08-31 根因:客户端只对未来 scheduledAt 补算 next_run_at;写 now=过去时间→不补算→调度器扫不到→卡死)
|
const d = new Date(Date.now() + 5 * 1000); // 未来 5 秒(08-31 根因:客户端只对未来 scheduledAt 补算 next_run_at;写 now=过去时间→不补算→调度器扫不到→卡死)
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
const scheduledAt = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
const scheduledAt = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||||
@@ -809,10 +894,17 @@ const server = http.createServer((req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { DatabaseSync } = require('node:sqlite');
|
const { DatabaseSync } = require('node:sqlite');
|
||||||
const db = new DatabaseSync(process.env.WORKBUDDY_DB || path.join(os.homedir(), '.workbuddy', 'workbuddy.db'), { readOnly: true });
|
const db = new DatabaseSync(process.env.WORKBUDDY_DB || path.join(os.homedir(), '.workbuddy', 'workbuddy.db'), { readOnly: true });
|
||||||
const auto = db.prepare('SELECT status, deleted_at, last_run_at FROM automations WHERE id=?').get(id);
|
const auto = db.prepare('SELECT status, deleted_at, last_run_at, created_at FROM automations WHERE id=?').get(id);
|
||||||
if (!auto || auto.deleted_at) { db.close(); return sendJSON(200, { ok: true, id, state: 'notfound' }); }
|
if (!auto || auto.deleted_at) { db.close(); return sendJSON(200, { ok: true, id, state: 'notfound' }); }
|
||||||
const st = db.prepare('SELECT running, last_error FROM automation_runtime_state WHERE automation_id=?').get(id);
|
const st = db.prepare('SELECT running, last_error FROM automation_runtime_state WHERE automation_id=?').get(id);
|
||||||
const runs = db.prepare("SELECT status FROM automation_runs WHERE automation_id=? ORDER BY created_at DESC LIMIT 1").get(id);
|
const runs = db.prepare("SELECT status FROM automation_runs WHERE automation_id=? ORDER BY created_at DESC LIMIT 1").get(id);
|
||||||
|
// 10-08:关联本任务拉起的后台会话(任务创建后 5 分钟内第一条自动化会话),读其日志产出进度
|
||||||
|
const t0 = Number(auto.created_at) || 0;
|
||||||
|
const sess = t0 ? db.prepare(
|
||||||
|
'SELECT id, title, cwd FROM sessions WHERE is_background_automation=1 AND created_at >= ? AND created_at <= ? ORDER BY created_at ASC LIMIT 1'
|
||||||
|
).get(t0 - 15000, t0 + 300000) : null;
|
||||||
|
const prog = sess ? readSessionProgress(sess.id, sess.cwd) : null;
|
||||||
|
const elapsedMs = t0 ? Date.now() - t0 : null;
|
||||||
db.close();
|
db.close();
|
||||||
let state = 'queued';
|
let state = 'queued';
|
||||||
if (st && Number(st.running) === 1) state = 'running';
|
if (st && Number(st.running) === 1) state = 'running';
|
||||||
@@ -824,7 +916,17 @@ const server = http.createServer((req, res) => {
|
|||||||
else if (['ERROR', 'CANCELLED', 'FAILED', 'INTERRUPTED'].includes(s)) state = 'error';
|
else if (['ERROR', 'CANCELLED', 'FAILED', 'INTERRUPTED'].includes(s)) state = 'error';
|
||||||
else state = 'pending'; // ACCEPTED / QUEUED / PENDING 等 = 排队等待执行
|
else state = 'pending'; // ACCEPTED / QUEUED / PENDING 等 = 排队等待执行
|
||||||
}
|
}
|
||||||
return sendJSON(200, { ok: true, id, state, error: (st && st.last_error) || null });
|
return sendJSON(200, {
|
||||||
|
ok: true, id, state, error: (st && st.last_error) || null,
|
||||||
|
elapsedMs,
|
||||||
|
sessionId: (sess && sess.id) || null,
|
||||||
|
sessionTitle: (sess && sess.title) || null,
|
||||||
|
lastAction: (prog && prog.lastAction) || '',
|
||||||
|
toolCalls: (prog && prog.toolCalls) || {},
|
||||||
|
updatedAgoMs: (prog && prog.updatedAgoMs) != null ? prog.updatedAgoMs : null,
|
||||||
|
stalled: !!(prog && prog.stalled),
|
||||||
|
logBytes: (prog && prog.logBytes) || 0,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return sendErr(500, '状态查询失败: ' + e.message);
|
return sendErr(500, '状态查询失败: ' + e.message);
|
||||||
}
|
}
|
||||||
@@ -888,7 +990,7 @@ const server = http.createServer((req, res) => {
|
|||||||
resp = await fetch(difyUrl + '/chat-messages', {
|
resp = await fetch(difyUrl + '/chat-messages', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
|
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'blocking', user: 'mcn-work-shop' }),
|
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'blocking', user: 'mcn-workshop' }),
|
||||||
signal: ctrl.signal,
|
signal: ctrl.signal,
|
||||||
});
|
});
|
||||||
} finally { clearTimeout(timer); }
|
} finally { clearTimeout(timer); }
|
||||||
@@ -915,10 +1017,7 @@ const server = http.createServer((req, res) => {
|
|||||||
if (!t) return '';
|
if (!t) return '';
|
||||||
return t.length <= 8 ? '***' : t.slice(0, 6) + '***' + t.slice(-4);
|
return t.length <= 8 ? '***' : t.slice(0, 6) + '***' + t.slice(-4);
|
||||||
};
|
};
|
||||||
const envKey = process.env.CODEBUDDY_API_KEY || '';
|
const cred = resolveCredentials({ apiKey: c.apiKey, authToken: c.authToken, credentialFile: c.credentialFile });
|
||||||
const envTok = process.env.CODEBUDDY_AUTH_TOKEN || '';
|
|
||||||
const effKey = c.apiKey || envKey;
|
|
||||||
const effTok = c.authToken || envTok;
|
|
||||||
const info = {
|
const info = {
|
||||||
backend,
|
backend,
|
||||||
cli: {
|
cli: {
|
||||||
@@ -926,10 +1025,11 @@ const server = http.createServer((req, res) => {
|
|||||||
pathConfigured: !!c.path,
|
pathConfigured: !!c.path,
|
||||||
model: c.model || '',
|
model: c.model || '',
|
||||||
internetEnvironment: c.internetEnvironment || '',
|
internetEnvironment: c.internetEnvironment || '',
|
||||||
credential: effTok ? 'CODEBUDDY_AUTH_TOKEN(' + mask(effTok) + ')'
|
credential: cred.source + (cred.apiKey || cred.authToken
|
||||||
: effKey ? 'CODEBUDDY_API_KEY(' + mask(effKey) + ')'
|
? ' (' + mask(cred.authToken || cred.apiKey) + ')' : ''),
|
||||||
: '(未配置)',
|
credentialFile: cred.credentialFile,
|
||||||
ready: !!(effTok || effKey),
|
credentialFileExists: (() => { try { return fs.existsSync(cred.credentialFile); } catch (e) { return false; } })(),
|
||||||
|
ready: !!(cred.authToken || cred.apiKey),
|
||||||
},
|
},
|
||||||
dify: {
|
dify: {
|
||||||
url: String(process.env.DIFY_URL || 'https://mydify.youmanvideo.com/v1'),
|
url: String(process.env.DIFY_URL || 'https://mydify.youmanvideo.com/v1'),
|
||||||
@@ -946,12 +1046,7 @@ const server = http.createServer((req, res) => {
|
|||||||
if (url.searchParams.get('probe') === '1' && backend === 'cli') {
|
if (url.searchParams.get('probe') === '1' && backend === 'cli') {
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
try {
|
try {
|
||||||
const r = await runCli({
|
const r = await runCli(cliBaseOpts({ prompt: '只回复两个字:成功', timeoutMs: 120000 }));
|
||||||
prompt: '只回复两个字:成功', cwd: ROOT, model: c.model,
|
|
||||||
apiKey: c.apiKey, authToken: c.authToken,
|
|
||||||
internetEnvironment: c.internetEnvironment, cliPath: c.path,
|
|
||||||
timeoutMs: 120000,
|
|
||||||
});
|
|
||||||
info.probe = {
|
info.probe = {
|
||||||
ok: r.ok, ms: Date.now() - t0,
|
ok: r.ok, ms: Date.now() - t0,
|
||||||
text: (r.text || '').slice(0, 100),
|
text: (r.text || '').slice(0, 100),
|
||||||
@@ -1030,18 +1125,7 @@ const server = http.createServer((req, res) => {
|
|||||||
// dify = 原链路(下方保留,未改动)
|
// dify = 原链路(下方保留,未改动)
|
||||||
// 两条链路对外都产出同一套 SSE 协议:{delta} / {replace} / {error},前端无需感知差异。
|
// 两条链路对外都产出同一套 SSE 协议:{delta} / {replace} / {error},前端无需感知差异。
|
||||||
if (aiBackend() === 'cli') {
|
if (aiBackend() === 'cli') {
|
||||||
const c = (CFG.ai && CFG.ai.cli) || {};
|
const base = cliBaseOpts({ prompt: query });
|
||||||
const base = {
|
|
||||||
prompt: query,
|
|
||||||
cwd: ROOT,
|
|
||||||
model: c.model,
|
|
||||||
apiKey: c.apiKey,
|
|
||||||
authToken: c.authToken,
|
|
||||||
internetEnvironment: c.internetEnvironment,
|
|
||||||
permissionMode: c.permissionMode,
|
|
||||||
cliPath: c.path,
|
|
||||||
timeoutMs: Math.max(30000, parseInt(c.timeoutMs, 10) || 300000),
|
|
||||||
};
|
|
||||||
if (stream) {
|
if (stream) {
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Content-Type': 'text/event-stream; charset=utf-8',
|
'Content-Type': 'text/event-stream; charset=utf-8',
|
||||||
@@ -1063,6 +1147,10 @@ const server = http.createServer((req, res) => {
|
|||||||
// 已产出的内容优先保留(模型中途失败 / 超时场景)
|
// 已产出的内容优先保留(模型中途失败 / 超时场景)
|
||||||
if (acc && r.text && r.text !== acc) write({ replace: r.text });
|
if (acc && r.text && r.text !== acc) write({ replace: r.text });
|
||||||
write({ error: r.error || 'CLI 执行失败' });
|
write({ error: r.error || 'CLI 执行失败' });
|
||||||
|
} else if (r.text && r.text !== acc) {
|
||||||
|
// 成功但「流过的内容」≠「终态正文」:典型是模型在联网搜索前先吐了一句过程话术,
|
||||||
|
// 后端已把工具调用前的部分切掉 → 这里用权威正文整体覆盖(前端原生支持 replace)
|
||||||
|
write({ replace: r.text });
|
||||||
}
|
}
|
||||||
log('AI 需求打磨(cli,stream): ' + accountName + ' | ' + acc.length + '字'
|
log('AI 需求打磨(cli,stream): ' + accountName + ' | ' + acc.length + '字'
|
||||||
+ (r.sessionId ? ' | session ' + r.sessionId : '') + (r.ok ? '' : ' | 失败: ' + r.error));
|
+ (r.sessionId ? ' | session ' + r.sessionId : '') + (r.ok ? '' : ' | 失败: ' + r.error));
|
||||||
@@ -1092,7 +1180,7 @@ const server = http.createServer((req, res) => {
|
|||||||
const difyResp = await fetch(difyUrl + '/chat-messages', {
|
const difyResp = await fetch(difyUrl + '/chat-messages', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
|
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'streaming', user: 'mcn-work-shop' }),
|
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'streaming', user: 'mcn-workshop' }),
|
||||||
signal: ctrl.signal,
|
signal: ctrl.signal,
|
||||||
});
|
});
|
||||||
if (!difyResp.ok || !difyResp.body) {
|
if (!difyResp.ok || !difyResp.body) {
|
||||||
@@ -1171,7 +1259,7 @@ const server = http.createServer((req, res) => {
|
|||||||
difyResp = await fetch(difyUrl + '/chat-messages', {
|
difyResp = await fetch(difyUrl + '/chat-messages', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
|
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'blocking', user: 'mcn-work-shop' }),
|
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'blocking', user: 'mcn-workshop' }),
|
||||||
signal: ctrl.signal,
|
signal: ctrl.signal,
|
||||||
});
|
});
|
||||||
} finally { clearTimeout(timer); }
|
} finally { clearTimeout(timer); }
|
||||||
@@ -1238,14 +1326,14 @@ const server = http.createServer((req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- 会话自动清理:mcn-work-shop 空间超 6 小时无活动的已结束会话自动清除 ----
|
// ---- 会话自动清理:mcn-workshop 空间超 6 小时无活动的已结束会话自动清除 ----
|
||||||
// 09-01 用户需求:工作台触发(cwd = 工作台目录 SESSION_CWD)的 AI 会话任务,超过 6 小时自动清除
|
// 09-01 用户需求:工作台触发(cwd = 工作台目录 SESSION_CWD)的 AI 会话任务,超过 6 小时自动清除
|
||||||
// 09-10 路径治本:cwd 由写死盘符改为 ROOT 动态推导(<技能根>/mcn-work-shop);
|
// 09-10 路径治本:cwd 由写死盘符改为 ROOT 动态推导(<技能根>/mcn-workshop);
|
||||||
// 清理模式放宽为 '%mcn-work%',同时覆盖历史会话(旧 mcn-workshop)与新建会话(mcn-work-shop)
|
// 清理模式放宽为 '%mcn-work%',同时覆盖历史会话(早期 D:\AgentSkill\mcn-workshop、迁移期 …\mcn-work-shop)与新建会话(…\V1.0\mcn-workshop)
|
||||||
// 判定:cwd 含 mcn-work + status=completed(执行中一律不删)+ 未删过 + 最后活动时间超过 6 小时
|
// 判定:cwd 含 mcn-work + status=completed(执行中一律不删)+ 未删过 + 最后活动时间超过 6 小时
|
||||||
// 方式:软删除(置 deleted_at),客户端左侧会话栏按 deleted_at 过滤即不再显示;可逆安全
|
// 方式:软删除(置 deleted_at),客户端左侧会话栏按 deleted_at 过滤即不再显示;可逆安全
|
||||||
const SESSION_TTL_MS = 6 * 3600 * 1000; // 6 小时
|
const SESSION_TTL_MS = 6 * 3600 * 1000; // 6 小时
|
||||||
const SESSION_CWD_PATTERN = '%mcn-work%'; // 兼容 SESSION_CWD(mcn-work-shop)与历史 cwd(mcn-workshop)
|
const SESSION_CWD_PATTERN = '%mcn-work%'; // 覆盖当前 SESSION_CWD(…\V1.0\mcn-workshop)与历史 cwd(早期 D:\AgentSkill\mcn-workshop、迁移期 …\mcn-work-shop)
|
||||||
const SESSION_CLEAN_INTERVAL_MS = 60 * 60 * 1000; // 每小时检查一次
|
const SESSION_CLEAN_INTERVAL_MS = 60 * 60 * 1000; // 每小时检查一次
|
||||||
|
|
||||||
function cleanupExpiredSessions() {
|
function cleanupExpiredSessions() {
|
||||||
@@ -1262,7 +1350,7 @@ function cleanupExpiredSessions() {
|
|||||||
AND COALESCE(last_activity_at, updated_at, created_at, 0) < ?
|
AND COALESCE(last_activity_at, updated_at, created_at, 0) < ?
|
||||||
`).run(now, SESSION_CWD_PATTERN, cutoff);
|
`).run(now, SESSION_CWD_PATTERN, cutoff);
|
||||||
db.close();
|
db.close();
|
||||||
if (Number(r.changes) > 0) log(`会话自动清理:已清除 mcn-work-shop 超 6 小时无活动的已完成会话 ${r.changes} 条`);
|
if (Number(r.changes) > 0) log(`会话自动清理:已清除 mcn-workshop 超 6 小时无活动的已完成会话 ${r.changes} 条`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('会话自动清理失败:', e.message);
|
console.error('会话自动清理失败:', e.message);
|
||||||
}
|
}
|
||||||
File renamed without changes.
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user