jianzi-content-workbench 0.2.29 → 0.2.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -8
- package/assets/dsh-workbench-overview.jpg +0 -0
- package/dsh/index.mjs +5 -7
- package/lib/client.js +33 -6
- package/package.json +2 -1
- package/shared/mcp-server-core.mjs +2 -3
- package/shared/tools.mjs +79 -0
package/README.md
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
# 见字 DSH 内容工作台
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
在[官方 DeepSeek Harness 桌面应用](https://github.com/deepseek-ai/deepseek-harness)里安装见字插件,把找选题、写文章、看内容日历和打开交付物放在一个入口中。按钮会先准备一条会话草稿;你检查并发送后,DSH Agent 再通过见字 MCP 工具执行。文章仍由当前 DSH 会话模型生成,业务记录保存在本机见字项目。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## 安装和开始使用
|
|
8
|
+
|
|
9
|
+
1. 打开官方 DeepSeek Harness 桌面应用的「插件 → 添加插件」。
|
|
10
|
+
2. 输入 `jianzi-content-workbench` 并安装。
|
|
11
|
+
3. 在已安装插件中启用见字内容工作台;若提示重启,按提示重启或重新加载。
|
|
12
|
+
4. 打开见字项目作为 DSH 当前会话的工作空间,并确认见字服务在本机运行。
|
|
13
|
+
5. 打开「见字内容工作台」,选择阶段和批次,检查自动准备的会话草稿后发送。
|
|
14
|
+
|
|
15
|
+
未连接时,可从状态卡准备启动任务。启动操作需要当前 DSH 会话能识别所选工作空间的本机绝对路径;如果项目目录尚不存在,请先通过检查环境流程确认初始化方案。启动不会自动克隆项目或安装 RSSHub。
|
|
16
|
+
|
|
17
|
+
**常见问题**
|
|
18
|
+
|
|
19
|
+
- **需要单独配置见字模型吗?** 不需要。默认使用当前 DSH 会话模型;见字内部模型仅用于明确选择见字自己的 AI 生成工具。
|
|
20
|
+
- **按钮会直接执行任务吗?** 不会。按钮只生成草稿,检查后由你发送。
|
|
21
|
+
- **安装时需要找插件市场吗?** 不需要。在「添加插件」中直接输入 npm 包名 `jianzi-content-workbench`。
|
|
22
|
+
|
|
23
|
+
更多连接与客户端设置见[Agent 接入指南](../../docs/agent-usage-guide.md)。
|
|
24
|
+
|
|
25
|
+
## 六段流程
|
|
6
26
|
|
|
7
27
|
| 阶段 | 面板动作 | 对应技能 / 门禁 |
|
|
8
28
|
| --- | --- | --- |
|
|
@@ -11,9 +31,12 @@
|
|
|
11
31
|
| 03 写作 | 准备文章草稿 | 同技能文章阶段:抓原文、确认角度、保存 draft;不保存 final |
|
|
12
32
|
| 04 交付 | 准备排版交付 | `jianzi-typeset-cover`:只接受已确认终稿,生成 HTML 排版与封面,返回两个入口 |
|
|
13
33
|
| 05 图文 | 准备图文 | `jianzi-social-cards`:工具/事件候选、故事板、锁定、视觉提交 |
|
|
34
|
+
| 06 复盘 | 上传导出表格后生成反馈草案 | 先检查附件;没有表格就请用户上传并停止。收到表格后用内容反馈 MCP 导入、匹配并关联本地正文,再刷新快照;关联未完成时不生成草案。草案必须由用户明确确认后才应用 |
|
|
14
35
|
|
|
15
36
|
面板输入只有三项:补充要求、批次 ID(可选)、候选 ID(可选)。留空时由 Agent 先列出现有批次或候选。任务以未发送草稿的形式进入原生会话,用户检查后按发送才执行。工作台未连接时,状态卡会提供「准备启动任务」按钮;提示词要求 Agent 使用当前 DSH 会话选中的工作空间绝对路径作为 `projectRoot`,调用 `jianzi_workbench_start` 启动,并等待服务就绪和项目指纹校验。若本机没有有效项目目录,检查提示会先询问是否克隆到空目录,确认后才初始化;启动工具本身不会克隆或安装项目。
|
|
16
37
|
|
|
38
|
+
首页的六个阶段任务都带有统一的下一步约定:结果完成后只推荐一个具体动作,并给出可直接回复的候选 ID、确认语或预览入口。存在用户选择/确认门禁时会停下来等回复;不以泛泛的「还需要什么帮助吗」结束。
|
|
39
|
+
|
|
17
40
|
## 实现
|
|
18
41
|
|
|
19
42
|
- `lib/client.js`:官方 Client Slots(`sidebar.panellist` + `main`)注册侧栏入口和整页面板;点击按钮调用 `uiWorkspace.startSession` 写入任务草稿。
|
|
@@ -35,6 +58,8 @@
|
|
|
35
58
|
|
|
36
59
|
DSH Agent 还可直接通过 MCP 工具 `jianzi_calendar_list` 读取内容日历,包含终稿文章、已交付图文和写作计划;可传 `month`(`YYYY-MM`)筛选月份。它复用见字现有 `/api/calendar` 查询,不修改日历。
|
|
37
60
|
|
|
61
|
+
内容反哺也通过首页「06 / 复盘」卡片和同一套 MCP 工具提供。点击卡片后,Agent 必须先检查本轮附件:没有公众号导出表格时,请用户上传并停止,不读取本地反馈、策略或已有草案,也不生成草案。收到表格后,导入工具会自动索引文章、匹配指标,并把本批次已确认的文章匹配关联到本地正文/图文文案,再刷新反馈快照/策略;工具结果会报告关联数、缺少本地文件数和失败数。关联未完成时,先处理结果中指出的问题,不用旧快照生成草案。用户请求生成草案时,Agent 在同一轮继续调用草案生成工具,不用用户再提示“下一步”。导入与 AI 草案生成保持分开,避免单次 MCP 调用超时。也可单独导入,工具会返回结构化 `next_action`。支持 CSV/TSV、旧版 XLS 原始文件,或 Agent 从附件完整提取的 CSV/TSV 文本;不支持 XLSX 二进制。文章指标表必须包含内容标题、发表日期、阅读人数、分享人数、阅读后关注人数等导入所需列。草案只保持待确认;应用工具才会写本地账号配置、标题技能或 Prompt。
|
|
62
|
+
|
|
38
63
|
Host 插件(`dsh/index.mjs`)通过官方 `ctx.webServer.register` 注册读取路由,供面板同源 `fetch` 使用;项目启动则通过 DSH 原生会话提示词调用 MCP 工具,不经过 Host 请求响应解析。提示词要求 Agent 从当前会话工作空间取得绝对路径并传给工具;MCP 子进程自身运行在 DSH profile 目录,不能用它的当前目录推断项目路径。
|
|
39
64
|
|
|
40
65
|
面板不再提供手填 ID 的输入框:点顶部「最近批次」chips 选择/取消批次,点「文章选题」列表选择/取消候选,顶部只显示当前选择并可一键清除。
|
|
@@ -43,7 +68,9 @@ Host 插件(`dsh/index.mjs`)通过官方 `ctx.webServer.register` 注册读
|
|
|
43
68
|
|
|
44
69
|
再往下是「内容日历」:按月展示文章、图文与写作计划,按日期分组并统计数量,可用 ‹ › 切换月份。
|
|
45
70
|
|
|
46
|
-
##
|
|
71
|
+
## 开发者本地构建
|
|
72
|
+
|
|
73
|
+
从仓库根目录运行:
|
|
47
74
|
|
|
48
75
|
```sh
|
|
49
76
|
node agent-integrations/dsh-workbench/build.mjs
|
|
@@ -51,14 +78,12 @@ cd agent-integrations/dsh-workbench
|
|
|
51
78
|
pnpm pack
|
|
52
79
|
```
|
|
53
80
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
当前插件可从源码构建 `.tgz` 并本地安装;npm 包尚未发布,也没有发布供用户添加的 DSH 插件索引条目。官方 Harness 支持通过插件管理器安装包名、Git 仓库、tarball 或本地路径;本仓库插件位于 monorepo 子目录,现阶段推荐按上述步骤本地打包安装。详见官方[插件打包与发布说明](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md)及[插件索引说明](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/plugin-market.md)。
|
|
81
|
+
本地 `.tgz` 安装适用于开发和调试;普通用户直接在插件管理器中输入 npm 包名即可。DSH 插件索引是另一种分发方式,目前未单独发布。详见官方[插件打包与发布说明](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md)及[插件索引说明](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/plugin-market.md)。
|
|
57
82
|
|
|
58
83
|
MCP 服务入口路径在 patch 中按 profile 写死(当前 `profiles/desktop`);在其他 profile 安装时需要同步修改。
|
|
59
84
|
|
|
60
85
|
## npm 正式发布
|
|
61
86
|
|
|
62
|
-
正式发布由仓库的 `Publish DSH plugin to npm` GitHub Actions 工作流执行。将插件 `package.json` 的版本更新并合入主分支后,推送对应标签 `dsh-plugin-v<版本号>`(例如 `dsh-plugin-v0.2.
|
|
87
|
+
正式发布由仓库的 `Publish DSH plugin to npm` GitHub Actions 工作流执行。将插件 `package.json` 的版本更新并合入主分支后,推送对应标签 `dsh-plugin-v<版本号>`(例如 `dsh-plugin-v0.2.30`)。工作流会核对标签和包版本、构建、生成 `.tgz`、上传 GitHub Actions 产物,并将同一个 `.tgz` 发布到 npm。不要用普通 CI 提交触发发布。
|
|
63
88
|
|
|
64
|
-
|
|
89
|
+
首次版本已发布到 npm Registry,GitHub Actions 已配置 **Trusted Publisher**:用户 `shiker1996`、仓库 `wechat-editroom`、工作流文件名 `publish-dsh-plugin.yml`,并允许直接发布。后续推送匹配包版本的标签时,工作流会通过 OIDC 发布,无需长期 npm 发布令牌。
|
|
Binary file
|
package/dsh/index.mjs
CHANGED
|
@@ -2,6 +2,10 @@ import { createJianziClient } from '../shared/client.mjs';
|
|
|
2
2
|
import { executeJianziTool } from '../shared/tools.mjs';
|
|
3
3
|
|
|
4
4
|
export const name = 'jianzi-content-workbench';
|
|
5
|
+
// Profile entries load concurrently, so resolving the Host HTTP carrier with
|
|
6
|
+
// ctx.get() at apply time can miss it and silently drop every panel route.
|
|
7
|
+
// Declaring the dependency lets cordis re-apply once webServer is active.
|
|
8
|
+
export const inject = ['webServer'];
|
|
5
9
|
|
|
6
10
|
const json = (payload, status = 200) => new Response(JSON.stringify(payload), {
|
|
7
11
|
status,
|
|
@@ -20,13 +24,7 @@ const failed = (error) => {
|
|
|
20
24
|
// registry. It reads Jianzi data through read-only Host routes. Startup actions
|
|
21
25
|
// are prepared as native DSH prompts so the Agent invokes the MCP tool.
|
|
22
26
|
export function apply(ctx) {
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
server = typeof ctx.get === 'function' ? ctx.get('webServer') : undefined;
|
|
26
|
-
} catch {
|
|
27
|
-
server = undefined;
|
|
28
|
-
}
|
|
29
|
-
if (!server || typeof server.register !== 'function') return;
|
|
27
|
+
const server = ctx.webServer;
|
|
30
28
|
|
|
31
29
|
const client = createJianziClient();
|
|
32
30
|
const read = async (tool, args) => {
|
package/lib/client.js
CHANGED
|
@@ -28,7 +28,7 @@ window.__ModuleLoader__.load({
|
|
|
28
28
|
startProject: 'Prepare start task',
|
|
29
29
|
startingProject: 'Preparing DSH conversation…',
|
|
30
30
|
projectReady: 'Startup task added to a native DSH conversation. Review it and press Send.',
|
|
31
|
-
promptStartProject: 'Start the Jianzi workbench for the currently selected DSH workspace. Determine the absolute path of this conversation\'s selected workspace and pass it as projectRoot to the MCP tool jianzi_workbench_start. Do not use the DSH profile/plugin directory as the project path. Wait for the tool to confirm the service is ready and the workspace fingerprint matches. If the workspace path is unavailable, ask me for it instead of guessing. If startup fails, report the returned error and stop; do not retry or run setup/configuration tasks. After success, report the project path and service address. Do not perform any other work.',
|
|
31
|
+
promptStartProject: 'Start the Jianzi workbench for the currently selected DSH workspace. Determine the absolute path of this conversation\'s selected workspace and pass it as projectRoot to the MCP tool jianzi_workbench_start. Do not use the DSH profile/plugin directory as the project path. Wait for the tool to confirm the service is ready and the workspace fingerprint matches. If the workspace path is unavailable, ask me for it instead of guessing. If startup fails, report the returned error and stop; do not retry or run setup/configuration tasks. After success, report the project path and service address, then end with one specific Next step recommending the environment-check card. If blocked, state the one correction needed. Do not perform any other work.',
|
|
32
32
|
recentBatches: 'Recent batches (click to fill)',
|
|
33
33
|
noBatches: 'No batches yet',
|
|
34
34
|
pickBatch: 'Fill',
|
|
@@ -92,7 +92,19 @@ window.__ModuleLoader__.load({
|
|
|
92
92
|
stage5Detail: 'Turn a repository or news candidate into a social card set: storyboard, lock, visual submission.',
|
|
93
93
|
stage5Action: 'Prepare card set',
|
|
94
94
|
stage5Gate: 'Needs a card candidate and a locked storyboard',
|
|
95
|
-
|
|
95
|
+
stage6: '06 / FEEDBACK',
|
|
96
|
+
stage6Title: 'Review content feedback',
|
|
97
|
+
stage6Detail: 'Upload a WeChat export table first; import it, link matched local content, then refresh signals and prepare a reviewable proposal.',
|
|
98
|
+
stage6Action: 'Prepare feedback proposal',
|
|
99
|
+
stage6Gate: 'Needs an attached WeChat export table; without one, ask for upload and stop',
|
|
100
|
+
nextStepContract: 'At the end, include a short "Next step" with one concrete action I can take now and the exact reply or ID to use. Never end with a generic "anything else?". If the next action needs my choice or approval, say so and wait; do not cross that gate.',
|
|
101
|
+
nextStep1: 'If setup checks pass, recommend starting topic research with stage 02. If blocked, name the single highest-priority correction and how I can unblock it.',
|
|
102
|
+
nextStep2: 'Show the candidate IDs and recommend one with a brief evidence-based reason. Ask me to reply with that ID; do not add a candidate or draft before I choose.',
|
|
103
|
+
nextStep3: 'After presenting the draft, recommend that I review it and reply with edits or explicit approval. Do not save a final or start typesetting before approval.',
|
|
104
|
+
nextStep4: 'After returning both preview links, recommend reviewing the article and cover. If there is no confirmed final, point back to stage 03. Never publish.',
|
|
105
|
+
nextStep5: 'After returning the card artifact, recommend reviewing the pages and copy and tell me how to request a specific revision. Never publish or replace a delivered artifact without my confirmation.',
|
|
106
|
+
nextStep6: 'After generation, identify the pending proposal by ID and recommend reviewing its diff. Only apply it if I explicitly ask; otherwise leave it pending.',
|
|
107
|
+
note: 'Each button opens a native DSH conversation with a task draft. Review and send it; after the task, the Agent gives one concrete next step.',
|
|
96
108
|
working: 'Preparing DSH conversation…',
|
|
97
109
|
failure: 'Could not prepare a DSH conversation. Check that this DSH profile provides the uiWorkspace client service.',
|
|
98
110
|
sent: 'Task draft prepared in the native DSH conversation. Review it and press Send.',
|
|
@@ -131,6 +143,7 @@ window.__ModuleLoader__.load({
|
|
|
131
143
|
promptArticle: 'Load the jianzi-topic-to-article skill and run its article stage. Fetch the candidate source first, read the candidate, account profile, and article-length settings, and show me the editorial angle for confirmation. Then save a draft and read it back. Do not save a final and do not start typesetting until I confirm the generated article.',
|
|
132
144
|
promptTypeset: 'Load the jianzi-typeset-cover skill and run typesetting and cover generation. Read the article documents first: continue only when a user-confirmed final exists. If only a draft exists, stop and explain. Generate the WeChat HTML, submit it for typesetting, generate the cover, and return both preview links. Do not publish to WeChat and do not call Jianzi internal-model typesetting tools.',
|
|
133
145
|
promptSocial: 'Load the jianzi-social-cards skill and run its card stage. Read the social ranking, pick a repository or event candidate, and confirm the material type with me if unclear. Then build the storyboard, let me approve it, lock it, and generate and submit the card visuals. Do not fake a hotspot candidate for a custom topic; use the custom social project flow instead.',
|
|
146
|
+
promptFeedback: 'This card requests a feedback review and adjustment proposal. First check whether this turn includes an attached WeChat export table. If there is no attachment, ask me to upload the table (CSV/TSV or legacy XLS; convert XLSX to CSV/TSV first) and stop. Do not call any content-feedback MCP tools: do not read local feedback or strategy, list or inspect existing proposals, or generate a proposal. Once I provide an attachment, identify its import type from the headers and import the complete data with the content-feedback import MCP tool (use exact Base64 bytes or faithfully extracted CSV/TSV text; never summarize, invent rows, or change values). The import tool also links confirmed matches from this batch to local article bodies/social copy before rebuilding the snapshot. If I attached multiple tables, import all of them before generating a proposal. Check the returned content_links counts and next_action; for article-metric imports, if linking failed or no matching local body/content was linked, stop and report that instead of generating from an older snapshot. An empty content_links result is expected for aggregate-only tables such as user growth or content trends. Only when the requested scope has usable evidence (article body evidence for article; matched social samples for social), read the refreshed feedback and strategy, then call the adjustment-generation MCP tool in this same turn. End with a concise status, evidence limits, proposal ID, and one specific next action. If the proposal is pending, invite me to review its diff or explicitly ask to apply it; never confirm or write it yourself. If headers are unsupported, stop and explain the single best correction. If I explicitly asked only to import without generating a proposal, finish after import and state the recommended generation tool and scope.',
|
|
134
147
|
},
|
|
135
148
|
zh: {
|
|
136
149
|
panelTitle: '见字内容工作台',
|
|
@@ -151,7 +164,7 @@ window.__ModuleLoader__.load({
|
|
|
151
164
|
startProject: '准备启动任务',
|
|
152
165
|
startingProject: '正在准备 DSH 会话…',
|
|
153
166
|
projectReady: '启动任务已放入 DSH 原生会话,检查后按发送即可执行。',
|
|
154
|
-
promptStartProject: '启动当前 DSH 会话所选工作空间对应的见字工作台。先确定本会话当前选中工作空间的本机绝对路径,并将该路径作为 projectRoot 参数传给见字 MCP 工具 jianzi_workbench_start。不要把 DSH profile
|
|
167
|
+
promptStartProject: '启动当前 DSH 会话所选工作空间对应的见字工作台。先确定本会话当前选中工作空间的本机绝对路径,并将该路径作为 projectRoot 参数传给见字 MCP 工具 jianzi_workbench_start。不要把 DSH profile 或插件目录当成项目路径。等待工具确认服务就绪且工作区指纹匹配;如果无法读取当前工作空间路径,先向我询问,不要猜路径。若启动失败,说明具体错误并停止,不要重试或继续配置。成功后汇报项目路径和服务地址,最后给出一个明确的「下一步」建议:先点「检查环境」;若仍有阻塞,只说最优先的一项修复。除此之外不要执行其他操作。',
|
|
155
168
|
recentBatches: '最近批次(点击填入)',
|
|
156
169
|
noBatches: '还没有批次',
|
|
157
170
|
pickBatch: '填入',
|
|
@@ -215,7 +228,19 @@ window.__ModuleLoader__.load({
|
|
|
215
228
|
stage5Detail: '把仓库或资讯候选做成一套图文:故事板、锁定、视觉提交。',
|
|
216
229
|
stage5Action: '准备图文',
|
|
217
230
|
stage5Gate: '需要有图文候选并锁定故事板',
|
|
218
|
-
|
|
231
|
+
stage6: '06 / 复盘',
|
|
232
|
+
stage6Title: '内容反馈与策略调整',
|
|
233
|
+
stage6Detail: '先上传公众号导出表格;导入并关联本地产物后,再刷新快照和生成可审阅草案。',
|
|
234
|
+
stage6Action: '生成反馈草案',
|
|
235
|
+
stage6Gate: '必须先附上公众号导出表格;没有附件时先请我上传并停止',
|
|
236
|
+
nextStepContract: '结尾必须单独给出「下一步」:只推荐一个现在可执行的动作,并写明我可以直接回复的内容或候选 ID。不要用「还需要什么帮助吗」之类泛泛收尾。若下一步需要我选择或确认,就明确停下等待;不要越过门禁。',
|
|
237
|
+
nextStep1: '环境检查通过时,建议我接着点 02「准备选题研判」;若未通过,只指出优先级最高的一项修复和解法。',
|
|
238
|
+
nextStep2: '列出候选 ID,并基于证据推荐一个。请我直接回复该 ID;我选择前不要加候选或写文章。',
|
|
239
|
+
nextStep3: '展示草稿后,建议我审阅并回复修改意见或明确确认。未经确认不要保存终稿或开始排版。',
|
|
240
|
+
nextStep4: '给出正文和封面预览链接后,建议我检查两项产物;若没有已确认终稿,明确引导回到 03「推进文章成稿」。绝不发布。',
|
|
241
|
+
nextStep5: '给出图文产物入口后,建议我检查分页与配套文案,并说明如何提出具体修改。未经确认不要发布或替换已有产物。',
|
|
242
|
+
nextStep6: '生成后给出待确认草案 ID,并建议我先查看 diff。只有我明确要求应用时才执行;否则保持待确认。',
|
|
243
|
+
note: '按钮会在 DSH 原生会话中准备任务草稿,不会自动发送。检查后发送;任务完成后 Agent 会给出一个具体的下一步建议。',
|
|
219
244
|
working: '正在准备 DSH 会话…',
|
|
220
245
|
failure: '无法准备 DSH 会话。请确认当前 DSH profile 提供 uiWorkspace 客户端服务。',
|
|
221
246
|
sent: '任务草稿已放入 DSH 原生会话,检查后按发送即可开始。',
|
|
@@ -254,6 +279,7 @@ window.__ModuleLoader__.load({
|
|
|
254
279
|
promptArticle: '加载 jianzi-topic-to-article 技能,执行文章阶段。先抓取候选原文,读取候选、账号画像和文章字数设置,把编辑角度给我确认;确认后保存 draft 并回读。未经我确认不要保存 final,也不要开始排版。',
|
|
255
280
|
promptTypeset: '加载 jianzi-typeset-cover 技能,执行排版与封面。先读取文章文档,只有存在我确认过的 final 才继续;只有 draft 就停下来说明。生成公众号 HTML 并提交排版,再生成封面,返回两个预览入口。不要发布到公众号,也不要调用见字内部模型排版工具。',
|
|
256
281
|
promptSocial: '加载 jianzi-social-cards 技能,执行图文阶段。先读图文榜单,选择仓库或事件候选;素材类型不明确时先问我。然后制定故事板、等我确认后锁定、生成并提交图文产物。自选主题不要伪装成热点候选,走自定义图文项目。',
|
|
282
|
+
promptFeedback: '这张卡片表示要复盘并生成调整草案。第一步先检查本轮是否附有公众号导出表格。没有附件时,只回复请我先上传表格(CSV/TSV 或旧版 XLS;XLSX 请先转成 CSV/TSV),然后停止;不要调用任何内容反哺 MCP 工具,不读取本地反馈或策略,不列出/查看已有草案,也不生成草案。收到附件后,先按表头判断导入类型,再用内容反哺导入 MCP 工具导入完整数据(传准确的 Base64 文件字节,或完整提取的 CSV/TSV 文本;不得汇总、编造行或改动数值)。导入工具会把本批次已确认的匹配关联到本地文章正文/图文文案,再刷新快照。如果附有多张表,先全部导入再考虑生成草案。检查返回的 content_links 和 next_action;对于文章表现表,如果有关联失败或没有匹配到本地正文/图文文案,停止并说明具体情况,不得拿旧快照生成草案。用户增长、内容趋势等汇总表没有 content_links 是正常的。所选范围有相应证据时(文章需有已关联正文;图文需有已匹配图文样本),读取刷新后的反馈和策略,然后在同一轮继续调用草案生成 MCP 工具。结尾明确当前状态、证据限制、草案 ID 和一个具体下一步。草案待确认时,提示我查看 diff 或明确要求应用;绝不自行确认或写入。如果表头不受支持,停止并给出最关键的修正建议。如果我明确只要求导入、不要求生成草案,则导入结束后告诉我建议调用的生成工具和范围。',
|
|
257
283
|
},
|
|
258
284
|
};
|
|
259
285
|
|
|
@@ -290,6 +316,7 @@ window.__ModuleLoader__.load({
|
|
|
290
316
|
{ id: 'article', key: '3', promptKey: 'promptArticle', fields: ['candidate', 'brief'] },
|
|
291
317
|
{ id: 'typeset', key: '4', promptKey: 'promptTypeset', fields: ['candidate', 'brief'] },
|
|
292
318
|
{ id: 'social', key: '5', promptKey: 'promptSocial', fields: ['batch', 'candidate', 'brief'] },
|
|
319
|
+
{ id: 'feedback', key: '6', promptKey: 'promptFeedback', fields: [] },
|
|
293
320
|
];
|
|
294
321
|
|
|
295
322
|
const styles = `
|
|
@@ -329,7 +356,7 @@ window.__ModuleLoader__.load({
|
|
|
329
356
|
.jianzi-desk__selection-item{display:inline-flex;align-items:center;gap:6px;min-height:26px;padding:4px 9px;border-radius:7px;border:1px solid rgba(215,166,111,.22);background:rgba(215,166,111,.07);color:var(--desk-copy)}
|
|
330
357
|
.jianzi-desk__clear{border:0;background:transparent;color:var(--desk-muted);font:600 12px/1.3 'Noto Sans SC','Microsoft YaHei',sans-serif;cursor:pointer;padding:4px}
|
|
331
358
|
.jianzi-desk__clear:hover{color:var(--desk-accent)}.jianzi-desk__clear:disabled{opacity:.3;cursor:default}
|
|
332
|
-
.jianzi-desk__flows{display:grid;grid-template-columns:repeat(
|
|
359
|
+
.jianzi-desk__flows{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:13px;margin-top:21px}
|
|
333
360
|
.jianzi-desk__flow{position:relative;display:flex;min-width:0;min-height:246px;flex-direction:column;padding:18px;border:1px solid var(--desk-line);border-radius:13px;background:linear-gradient(155deg,rgba(255,255,255,.055),rgba(255,255,255,.018) 72%);box-shadow:0 8px 20px rgba(0,0,0,.10);transition:transform .18s ease,border-color .18s ease,background .18s ease,box-shadow .18s ease}
|
|
334
361
|
.jianzi-desk__flow:hover{transform:translateY(-2px);border-color:rgba(215,166,111,.48);background:linear-gradient(155deg,rgba(215,166,111,.09),rgba(255,255,255,.022) 72%);box-shadow:0 14px 28px rgba(0,0,0,.17)}
|
|
335
362
|
.jianzi-desk__number{display:flex;align-items:center;gap:8px;color:var(--desk-accent);font-size:11px;font-weight:750;letter-spacing:.1em}
|
|
@@ -463,7 +490,7 @@ window.__ModuleLoader__.load({
|
|
|
463
490
|
if (stage.fields.includes('batch') && values.batch.trim()) extra.push(`${tr('batchLead')}${values.batch.trim()}`);
|
|
464
491
|
if (stage.fields.includes('candidate') && values.candidate.trim()) extra.push(`${tr('candidateLead')}${values.candidate.trim()}`);
|
|
465
492
|
if (values.context) extra.push(values.context);
|
|
466
|
-
return [base, ...extra].join('\n\n');
|
|
493
|
+
return [base, ...extra, tr('nextStepContract'), tr(`nextStep${stage.key}`)].join('\n\n');
|
|
467
494
|
}
|
|
468
495
|
|
|
469
496
|
function Panel({ tr, startTask }) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jianzi-content-workbench",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.30",
|
|
4
4
|
"icon": "./icon.svg",
|
|
5
5
|
"description": "DSH content workbench panel that starts native Agent sessions backed by Jianzi MCP tools.",
|
|
6
6
|
"repository": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"dsh",
|
|
22
22
|
"lib",
|
|
23
23
|
"shared",
|
|
24
|
+
"assets",
|
|
24
25
|
"mcp-server.mjs",
|
|
25
26
|
"cordis.patch.yml",
|
|
26
27
|
"README.md",
|
|
@@ -2,9 +2,8 @@ import readline from 'node:readline';
|
|
|
2
2
|
|
|
3
3
|
const protocolVersion = '2025-03-26';
|
|
4
4
|
const supportedProtocolVersions = new Set([protocolVersion, '2024-11-05']);
|
|
5
|
-
|
|
6
5
|
/** Start the shared stdio MCP transport for Jianzi tool implementations. */
|
|
7
|
-
export function startJianziMcpServer({ executeTool, tools, input = process.stdin, output = process.stdout }) {
|
|
6
|
+
export function startJianziMcpServer({ executeTool, tools, instructions, input = process.stdin, output = process.stdout }) {
|
|
8
7
|
const send = (message) => output.write(`${JSON.stringify(message)}\n`);
|
|
9
8
|
const result = (id, value) => send({ jsonrpc: '2.0', id, result: value });
|
|
10
9
|
const rpcError = (id, code, message) => send({ jsonrpc: '2.0', id, error: { code, message } });
|
|
@@ -14,7 +13,7 @@ export function startJianziMcpServer({ executeTool, tools, input = process.stdin
|
|
|
14
13
|
if (method === 'notifications/initialized' || method === 'notifications/cancelled') return;
|
|
15
14
|
if (method === 'initialize') {
|
|
16
15
|
const negotiated = supportedProtocolVersions.has(params.protocolVersion) ? params.protocolVersion : protocolVersion;
|
|
17
|
-
result(id, { protocolVersion: negotiated, capabilities: { tools: {} }, serverInfo: { name: 'jianzi-workbench', version: '1.0.0' } });
|
|
16
|
+
result(id, { protocolVersion: negotiated, capabilities: { tools: {} }, serverInfo: { name: 'jianzi-workbench', version: '1.0.0' }, ...(instructions ? { instructions } : {}) });
|
|
18
17
|
return;
|
|
19
18
|
}
|
|
20
19
|
if (method === 'ping') { result(id, {}); return; }
|
package/shared/tools.mjs
CHANGED
|
@@ -5,6 +5,11 @@ import { spawn } from 'node:child_process';
|
|
|
5
5
|
|
|
6
6
|
const id = (description, required = true) => ({ type: 'integer', ...(required ? { minimum: 1 } : {}), description });
|
|
7
7
|
const string = (description, extra = {}) => ({ type: 'string', description, ...extra });
|
|
8
|
+
const positiveId = (value, field) => {
|
|
9
|
+
const parsed = Number(value);
|
|
10
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) throw Object.assign(new Error(`${field} 必须是正整数`), { code: 'INVALID_ARGUMENT' });
|
|
11
|
+
return parsed;
|
|
12
|
+
};
|
|
8
13
|
const sensitiveField = /^(?:api[_-]?key|token|secret|password|authorization|cookie|credential|credentials)$/i;
|
|
9
14
|
|
|
10
15
|
function redactSensitive(value, key = '') {
|
|
@@ -48,6 +53,13 @@ export const JIANZI_TOOLS = [
|
|
|
48
53
|
{ name: 'jianzi_workbench_start', description: '当用户要求启动已停止的本机见字工作台,或 jianzi_doctor 报告服务未运行时,调用项目官方启动脚本并等待服务 ready,再执行健康检查。不会打开浏览器或修改业务数据。源码 MCP 默认使用当前项目根目录;打包/独立 MCP 可传项目绝对路径 projectRoot,或设置 JIANZI_PROJECT_ROOT。', inputSchema: { type: 'object', properties: { projectRoot: string('见字项目的本机绝对路径。源码集成可省略;DSH/npm 包未配置 JIANZI_PROJECT_ROOT 时必须提供。') } }, readOnlyHint: false },
|
|
49
54
|
{ name: 'jianzi_account_profile', description: '读取账号画像,用于选题定位、语气和受众判断。', inputSchema: { type: 'object', properties: {} }, readOnlyHint: true },
|
|
50
55
|
{ name: 'jianzi_account_profile_update', description: '保存用户确认过的账号画像。至少包含公众号名称、简介、核心读者和一个内容支柱。', inputSchema: { type: 'object', properties: { context: { type: 'object', description: '符合 account-context.json 格式的账号画像字段。' } }, required: ['context'] }, readOnlyHint: false },
|
|
56
|
+
{ name: 'jianzi_content_feedback_get', description: '读取最新公众号内容反馈快照与正文分析计数;用于解释历史题材、标题和正文结构信号。', inputSchema: { type: 'object', properties: {} }, readOnlyHint: true },
|
|
57
|
+
{ name: 'jianzi_content_feedback_strategy', description: '读取跨周期策略状态、账号定位字段映射候选和渠道数据限制。只读,不写入账号配置。', inputSchema: { type: 'object', properties: {} }, readOnlyHint: true },
|
|
58
|
+
{ name: 'jianzi_content_feedback_import', description: '导入用户明确提供的公众号后台表格;自动合并数据、索引本地文章并匹配,再将本次批次已确认的匹配关联到本地正文/图文文案,之后才刷新内容反馈快照和策略。返回正文关联成功、失败与缺少本地产物的数量;未关联的内容不会被当作正文证据。只产生数据快照与建议,不生成或应用配置/Prompt 草案。支持 CSV/TSV、旧版 XLS 或从附件准确提取的 CSV/TSV 文本;不支持直接解析 XLSX 二进制。', inputSchema: { type: 'object', properties: { fileName: { type: 'string', minLength: 1, maxLength: 255, description: '上传文件名。CSV 文本转换自 XLSX 时可保留原文件名;csvText 会作为文本解析。' }, importType: { type: 'string', enum: ['user_growth', 'notified_articles', 'unnotified_articles', 'content_trends', 'regular_readers'], description: '公众号导出类别;根据表头选择。文章表现表使用 notified_articles 或 unnotified_articles。' }, data: { type: 'string', maxLength: 8000000, description: '文件原始字节的 Base64,适用于 CSV/TSV 和旧版 XLS;与 csvText 二选一。' }, csvText: { type: 'string', maxLength: 8000000, description: '从上传附件准确提取的完整 CSV/TSV 文本;保留所有行与数字,不做汇总或推算。与 data 二选一。' } }, required: ['fileName', 'importType'] }, readOnlyHint: false },
|
|
59
|
+
{ name: 'jianzi_content_feedback_adjustments_list', description: '列出内容反哺调整草案及可编辑正文 Prompt。', inputSchema: { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 100, description: '返回数量,默认 20。' } } }, readOnlyHint: true },
|
|
60
|
+
{ name: 'jianzi_content_feedback_adjustment_generate', description: '调用工作台两阶段模型生成文章或图文内容反哺草案。草案只保存为待确认记录,不会自动写入账号配置、标题技能、正文 Prompt 或图文技能。', inputSchema: { type: 'object', properties: { scope: { type: 'string', enum: ['article', 'social'], description: '反馈范围,默认 article;social 用于图文故事板和文案技能。' }, feedbackSnapshotId: { type: 'integer', minimum: 1, description: '可选反馈快照 ID;省略时使用最新快照。' }, provider: string('可选的工作台模型服务商 ID。') } }, readOnlyHint: false },
|
|
61
|
+
{ name: 'jianzi_content_feedback_adjustment_confirm', description: '将待确认内容反哺草案写入本地账号配置、标题技能、文章 Prompt 或图文技能。仅在用户明确要求应用该草案后调用。', inputSchema: { type: 'object', properties: { adjustmentId: id('待确认调整草案 ID。') }, required: ['adjustmentId'] }, readOnlyHint: false },
|
|
62
|
+
{ name: 'jianzi_content_feedback_adjustment_reject', description: '拒绝一条待确认内容反哺草案,不修改配置或技能文件。', inputSchema: { type: 'object', properties: { adjustmentId: id('待确认调整草案 ID。') }, required: ['adjustmentId'] }, readOnlyHint: false },
|
|
51
63
|
{ name: 'jianzi_settings_summary', description: '读取非敏感设置与模型配置状态;不会返回 API Key、API 地址或调用日志。', inputSchema: { type: 'object', properties: {} }, readOnlyHint: true },
|
|
52
64
|
{ name: 'jianzi_collectors_list', description: '列出可用采集器及其配置状态;秘密字段只返回是否已配置。', inputSchema: { type: 'object', properties: {} }, readOnlyHint: true },
|
|
53
65
|
{ name: 'jianzi_collection_capabilities', description: '读取采集能力状态,包含各来源组就绪情况和 RSSHub 状态。', inputSchema: { type: 'object', properties: {} }, readOnlyHint: true },
|
|
@@ -296,6 +308,73 @@ export async function executeJianziTool(client, name, input = {}, context = {})
|
|
|
296
308
|
}
|
|
297
309
|
case 'jianzi_account_profile': return client.get('/api/system/account-context');
|
|
298
310
|
case 'jianzi_account_profile_update': return client.put('/api/system/account-context', input.context || {});
|
|
311
|
+
case 'jianzi_content_feedback_get': return client.get('/api/wechat/feedback');
|
|
312
|
+
case 'jianzi_content_feedback_strategy': return client.get('/api/wechat/strategy');
|
|
313
|
+
case 'jianzi_content_feedback_import': {
|
|
314
|
+
const hasData = typeof input.data === 'string' && input.data.trim();
|
|
315
|
+
const hasCsvText = typeof input.csvText === 'string' && input.csvText.trim();
|
|
316
|
+
if (Boolean(hasData) === Boolean(hasCsvText)) throw Object.assign(new Error('请在 data(Base64 文件)和 csvText(CSV/TSV 文本)中恰好提供一种'), { code: 'INVALID_ARGUMENT' });
|
|
317
|
+
if (hasData && /\.xlsx?$/i.test(input.fileName || '') && !/\.xls$/i.test(input.fileName || '')) throw Object.assign(new Error('当前导入器支持旧版 .xls,不支持 XLSX 二进制;请在外部 Agent 中将附件准确提取为 csvText 后重试'), { code: 'UNSUPPORTED_FILE_FORMAT' });
|
|
318
|
+
const imported = await client.post('/api/wechat/import', {
|
|
319
|
+
fileName: String(input.fileName || '').trim(),
|
|
320
|
+
importType: input.importType,
|
|
321
|
+
data: hasData ? input.data.trim() : Buffer.from(input.csvText, 'utf8').toString('base64'),
|
|
322
|
+
});
|
|
323
|
+
const contentLinks = await client.post('/api/wechat/content-links/relink', { importBatchId: imported.id });
|
|
324
|
+
const rebuilt = await client.post('/api/wechat/feedback/rebuild', {});
|
|
325
|
+
const [strategy, currentFeedback, review] = await Promise.all([
|
|
326
|
+
client.get('/api/wechat/strategy'),
|
|
327
|
+
client.get('/api/wechat/feedback'),
|
|
328
|
+
client.get('/api/wechat/review'),
|
|
329
|
+
]);
|
|
330
|
+
const articleEvidenceCount = Number(rebuilt.extracted || 0);
|
|
331
|
+
const socialEvidenceCount = Number(review.review_tracks?.social?.content_feedback?.linked_social_count || 0);
|
|
332
|
+
const isArticleMetricsImport = ['notified_articles', 'unnotified_articles'].includes(input.importType);
|
|
333
|
+
const importedArticleEvidenceCount = Number(contentLinks.linked || 0);
|
|
334
|
+
const importedSocialEvidenceCount = Number(contentLinks.social_copy || 0);
|
|
335
|
+
const contentLinkBlocked = isArticleMetricsImport && (
|
|
336
|
+
(Number(imported.row_count || 0) > 0 && Number(contentLinks.total || 0) === 0)
|
|
337
|
+
|| Number(contentLinks.needs_external || 0) > 0
|
|
338
|
+
|| Number(contentLinks.failed || 0) > 0
|
|
339
|
+
);
|
|
340
|
+
const availableScopes = {
|
|
341
|
+
article: articleEvidenceCount > 0 && (!isArticleMetricsImport || importedArticleEvidenceCount > 0),
|
|
342
|
+
social: socialEvidenceCount > 0 && (!isArticleMetricsImport || importedSocialEvidenceCount > 0),
|
|
343
|
+
};
|
|
344
|
+
const hasFeedbackEvidence = !contentLinkBlocked && (availableScopes.article || availableScopes.social);
|
|
345
|
+
return {
|
|
346
|
+
imported: { id: imported.id, file_name: imported.file_name, import_type: imported.import_type, format: imported.format, row_count: imported.row_count },
|
|
347
|
+
index: imported.index,
|
|
348
|
+
matches: imported.matches,
|
|
349
|
+
content_links: { total: contentLinks.total, linked: contentLinks.linked, social_copy: contentLinks.social_copy, needs_external: contentLinks.needs_external, failed: contentLinks.failed, failures: contentLinks.failures || [] },
|
|
350
|
+
feedback: { ...rebuilt.feedback, extracted_count: rebuilt.extracted, article_analysis_count: currentFeedback.stats?.linked_articles || 0, feature_count: currentFeedback.stats?.features || 0 },
|
|
351
|
+
strategy,
|
|
352
|
+
next_action: hasFeedbackEvidence
|
|
353
|
+
? { type: 'generate_adjustment_draft', tool: 'jianzi_content_feedback_adjustment_generate', available_scopes: availableScopes, default_scope: availableScopes.article ? 'article' : 'social', when: 'If the user asked to generate an adjustment proposal, call this tool in the same turn using the requested scope; otherwise present it as the recommended next step.', writes_files: false }
|
|
354
|
+
: { type: 'resolve_article_matches', message: contentLinkBlocked ? 'This article-metric import has unmatched or unlinked local content. Resolve its matching/content-link failures before generating a proposal from this import.' : 'No linked article bodies or matched social samples were available for extraction. Review article matching and linked content before treating the snapshot as evidence for a draft.', writes_files: false },
|
|
355
|
+
next_step: hasFeedbackEvidence
|
|
356
|
+
? `下一步:${availableScopes.article ? `文章有 ${articleEvidenceCount} 条可分析正文,本批次新关联 ${importedArticleEvidenceCount} 条` : ''}${availableScopes.article && availableScopes.social ? ';' : ''}${availableScopes.social ? `图文有 ${socialEvidenceCount} 条匹配样本,本批次新关联 ${importedSocialEvidenceCount} 条` : ''}。如用户本轮要求生成草案,按其范围继续调用 jianzi_content_feedback_adjustment_generate;否则建议选择文章或图文范围。`
|
|
357
|
+
: contentLinkBlocked
|
|
358
|
+
? `下一步:先处理本批次正文关联。匹配产物 ${contentLinks.total} 条,成功关联正文 ${contentLinks.linked} 条、图文文案 ${contentLinks.social_copy} 条,缺少本地文件 ${contentLinks.needs_external} 条,关联失败 ${contentLinks.failed} 条。处理后刷新快照,再生成草案。`
|
|
359
|
+
: '下一步:先检查公众号文章/图文匹配和已关联正文;当前没有可用于草案的关联内容证据。',
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
case 'jianzi_content_feedback_adjustments_list': return client.get(`/api/wechat/feedback/adjustments?limit=${Math.min(100, Math.max(1, Number(input.limit) || 20))}`);
|
|
363
|
+
case 'jianzi_content_feedback_adjustment_generate': {
|
|
364
|
+
const draft = await client.post('/api/wechat/feedback/adjustments/generate', {
|
|
365
|
+
scope: input.scope === 'social' ? 'social' : 'article',
|
|
366
|
+
...(Number.isInteger(input.feedbackSnapshotId) && input.feedbackSnapshotId > 0 ? { feedbackSnapshotId: input.feedbackSnapshotId } : {}),
|
|
367
|
+
...(typeof input.provider === 'string' && input.provider.trim() ? { provider: input.provider.trim() } : {}),
|
|
368
|
+
});
|
|
369
|
+
return {
|
|
370
|
+
...draft,
|
|
371
|
+
next_action: draft.status === 'pending'
|
|
372
|
+
? { type: 'review_adjustment_draft', label: `检查草案 ${draft.id} 的 diff`, after_review: `只有明确要求应用草案 ${draft.id} 时才调用 jianzi_content_feedback_adjustment_confirm。`, writes_files: false }
|
|
373
|
+
: { type: 'report_no_change', label: '说明当前没有可安全应用的规则改动,并指出证据或门槛限制。', writes_files: false },
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
case 'jianzi_content_feedback_adjustment_confirm': return client.post(`/api/wechat/feedback/adjustments/${positiveId(input.adjustmentId, 'adjustmentId')}/confirm`, {});
|
|
377
|
+
case 'jianzi_content_feedback_adjustment_reject': return client.post(`/api/wechat/feedback/adjustments/${positiveId(input.adjustmentId, 'adjustmentId')}/reject`, {});
|
|
299
378
|
case 'jianzi_settings_summary': {
|
|
300
379
|
const [settings, models] = await Promise.all([client.get('/api/system/settings'), client.get('/api/models')]);
|
|
301
380
|
return {
|