@youweichen/pi-harness 1.0.0 → 1.2.0
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/LICENSE +1 -1
- package/README.md +14 -0
- package/dist/server/agent-service.js +87 -7
- package/dist/server/control-socket.js +1 -0
- package/dist/server/document-conversion/extension.js +164 -0
- package/dist/server/document-conversion/python/bridge.py +424 -0
- package/dist/server/document-conversion/python/fixtures.py +201 -0
- package/dist/server/document-conversion/python/native_code.py +149 -0
- package/dist/server/document-conversion/runtime.js +321 -0
- package/dist/server/document-conversion/service.js +340 -0
- package/dist/server/document-conversion/settings.js +63 -0
- package/dist/server/extensions-worker.js +3 -2
- package/dist/server/files-service.js +8 -16
- package/dist/server/index.js +9 -3
- package/dist/server/native-tools.js +4 -0
- package/dist/server/node-agent.js +262 -0
- package/dist/server/node-workbench.js +62 -1
- package/dist/server/okf/extension.js +118 -0
- package/dist/server/okf/markdown-sources.js +58 -0
- package/dist/server/okf/service.js +713 -0
- package/dist/server/okf/storage.js +165 -0
- package/dist/server/okf/types.js +1 -0
- package/dist/server/plan/extension.js +4 -4
- package/dist/server/plan/state.js +11 -1
- package/dist/server/slash-commands.js +6 -0
- package/dist/server/wiki-links.js +24 -0
- package/dist/server/wiki-routes.js +8 -1
- package/dist/server/wiki-service.js +50 -5
- package/package.json +2 -2
- package/web/dist/assets/NodeWorkbench-DM6YTS5e.js +3 -0
- package/web/dist/assets/{TerminalPanel-CWh6vw_N.js → TerminalPanel-aFJylyxg.js} +1 -1
- package/web/dist/assets/{abnfDiagram-VCTEODGH-D5Ti0Kpa.js → abnfDiagram-VCTEODGH-CcM7f7oX.js} +1 -1
- package/web/dist/assets/{arc-DKJilLd6.js → arc-Bv7vS5l5.js} +1 -1
- package/web/dist/assets/{architectureDiagram-5GKGNRK7-Cf0w38g-.js → architectureDiagram-5GKGNRK7-Cl8QcB3w.js} +1 -1
- package/web/dist/assets/{blockDiagram-I7D4REHJ-BOTLWLIL.js → blockDiagram-I7D4REHJ-DarS2-4G.js} +1 -1
- package/web/dist/assets/{c4Diagram-7LVT6UL2-CLCCoJs3.js → c4Diagram-7LVT6UL2-BY0_ttBt.js} +1 -1
- package/web/dist/assets/channel-DJGMgVTh.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-BHjga2Ge.js → chunk-2Q5K7J3B-Cz-zS7_X.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-CQJ5VfDZ.js → chunk-5VM5RSS4-B7AyqkmI.js} +1 -1
- package/web/dist/assets/{chunk-F27PBJKO-C67qoPJH.js → chunk-F27PBJKO-B7hVduzL.js} +1 -1
- package/web/dist/assets/{chunk-IMKFNOWR-CgXXGwI8.js → chunk-IMKFNOWR-DNGJqYn5.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-D0RPyByP.js → chunk-JWPE2WC7-B5iJgXvl.js} +1 -1
- package/web/dist/assets/{chunk-POPQ4Y6H-BeYC9s9x.js → chunk-POPQ4Y6H-BvWCkriR.js} +1 -1
- package/web/dist/assets/{chunk-SVP7TREG-eQPl4UBQ.js → chunk-SVP7TREG-BcdlBuOQ.js} +1 -1
- package/web/dist/assets/{chunk-TICWLB2K-DT73arPU.js → chunk-TICWLB2K-B1igqjQd.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-B4FwQcP-.js → chunk-XXDRQBXY-DKGDquaw.js} +1 -1
- package/web/dist/assets/classDiagram-ZZMXUADV-D9nxgF0M.js +1 -0
- package/web/dist/assets/classDiagram-v2-VYDZK3BY-D9nxgF0M.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-Bnn5nlQN.js → cose-bilkent-JH36ORCC-BU_9nu88.js} +1 -1
- package/web/dist/assets/{cynefin-OW5HDTMX-CStWX9kE.js → cynefin-OW5HDTMX-DY0BDLjc.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-BMVHC03f.js → cynefinDiagram-5FMLGOSQ-B-zM25Fk.js} +1 -1
- package/web/dist/assets/{dagre-GXQ25YYZ-ClSaO4ml.js → dagre-GXQ25YYZ-BxooDyBc.js} +1 -1
- package/web/dist/assets/{diagram-S7CK7UJ4-C43LL08C.js → diagram-S7CK7UJ4-CCO-8de9.js} +1 -1
- package/web/dist/assets/{diagram-UQ7AKVKN-BPVn_ySW.js → diagram-UQ7AKVKN-CmGsH525.js} +1 -1
- package/web/dist/assets/{diagram-VSXAHHWV-C34FFhRi.js → diagram-VSXAHHWV-CSQJYo3m.js} +1 -1
- package/web/dist/assets/{diagram-VX7I27RA-BRk4bncf.js → diagram-VX7I27RA-CwuJsF66.js} +1 -1
- package/web/dist/assets/{diagram-Z3DM3KII-O5hYPLEd.js → diagram-Z3DM3KII-BF13T_lj.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-PWID7BFC-DVGEBm99.js → ebnfDiagram-PWID7BFC-ZtoDLro6.js} +1 -1
- package/web/dist/assets/{erDiagram-RLTQ6QDP-M-BLpTEV.js → erDiagram-RLTQ6QDP-BBo-qmNH.js} +1 -1
- package/web/dist/assets/{flowDiagram-HODETNUW-BwdLopEx.js → flowDiagram-HODETNUW-DAq9qdP8.js} +1 -1
- package/web/dist/assets/{ganttDiagram-EL5Y4UJY-DYxx2ppA.js → ganttDiagram-EL5Y4UJY-C53eRlMu.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-DUU0wMYB.js → gitGraphDiagram-WWUBYQGX-BviBMJxj.js} +1 -1
- package/web/dist/assets/index-Bab44wX8.js +276 -0
- package/web/dist/assets/index-C3vDFSEV.css +10 -0
- package/web/dist/assets/{infoDiagram-27XIBGKW-CFaW5C0C.js → infoDiagram-27XIBGKW-DI95SO3j.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-5VMMS53U--ENP1MTB.js → ishikawaDiagram-5VMMS53U-Cl8IDXwf.js} +1 -1
- package/web/dist/assets/{journeyDiagram-3NMN7TZE-DgnAzSTj.js → journeyDiagram-3NMN7TZE-Co6aJeTZ.js} +1 -1
- package/web/dist/assets/{kanban-definition-UXKFOSKX-_STZN1vU.js → kanban-definition-UXKFOSKX-CknQQpwW.js} +1 -1
- package/web/dist/assets/{layout-CkUClkXt.js → layout-CFtUmtTW.js} +1 -1
- package/web/dist/assets/{linear-sYHiUOxa.js → linear-mRlGUAKy.js} +1 -1
- package/web/dist/assets/{markdown-vNTCZJ6d.js → markdown-BhJ_LInD.js} +1 -1
- package/web/dist/assets/{mermaid.core-B8fKGWsc.js → mermaid.core-CvQEO3ue.js} +5 -5
- package/web/dist/assets/{mindmap-definition-YA3MSWOX-DP0BYOEx.js → mindmap-definition-YA3MSWOX-DpqhsxUl.js} +1 -1
- package/web/dist/assets/{pegDiagram-XKGWAZYB-DkWTmZre.js → pegDiagram-XKGWAZYB-C9eawB5u.js} +1 -1
- package/web/dist/assets/{pieDiagram-E7YTZNPT-DfZgMV4u.js → pieDiagram-E7YTZNPT-8vaqXVD6.js} +1 -1
- package/web/dist/assets/{quadrantDiagram-AXDQQJYC-Djl4TT7q.js → quadrantDiagram-AXDQQJYC-Diwr8ynm.js} +1 -1
- package/web/dist/assets/{railroadDiagram-O6MQD6OU-DbKF6zXn.js → railroadDiagram-O6MQD6OU-krW50Cgi.js} +1 -1
- package/web/dist/assets/{react-BNj3n7jX.js → react-BddUxV-9.js} +1 -1
- package/web/dist/assets/{requirementDiagram-BXWQKSXE-idola5qb.js → requirementDiagram-BXWQKSXE-BPLI6eMG.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-P5KCCOFB-KA0hQOle.js → sankeyDiagram-P5KCCOFB-Bix_elmH.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-BXSKvxzP.js → sequenceDiagram-WJ2MYXX4-0m0xaUKi.js} +1 -1
- package/web/dist/assets/{sizeCapture-INFHLROL-C_Ewv3S5.js → sizeCapture-INFHLROL-BtzJ_oOR.js} +1 -1
- package/web/dist/assets/{stateDiagram-D77RDMKH-DIf5X8oo.js → stateDiagram-D77RDMKH-Clq5bN0v.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-MP3YSRHH-Bn1oW4gp.js +1 -0
- package/web/dist/assets/{swimlanes-42K2YHIH-Cx_Rc8M2.js → swimlanes-42K2YHIH-11MDGnYA.js} +1 -1
- package/web/dist/assets/swimlanesDiagram-VR7AAH4N-BW96Edgo.js +8 -0
- package/web/dist/assets/{timeline-definition-24CTP7MA-BRrDhWVz.js → timeline-definition-24CTP7MA-BeQfWP95.js} +1 -1
- package/web/dist/assets/{vennDiagram-4TSXK5OY-Ad6G0YwD.js → vennDiagram-4TSXK5OY-BMYkX0KL.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-VM6X3IG4-X6-ENpSi.js → wardleyDiagram-VM6X3IG4-BV3fTUkU.js} +1 -1
- package/web/dist/assets/{xychartDiagram-S5SC5T6Z-DhegNS6d.js → xychartDiagram-S5SC5T6Z-Ct7ezLnc.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/dist/assets/NodeWorkbench-C4U1PldE.js +0 -3
- package/web/dist/assets/channel-D8Hd7T4P.js +0 -1
- package/web/dist/assets/classDiagram-ZZMXUADV-B_btgYRB.js +0 -1
- package/web/dist/assets/classDiagram-v2-VYDZK3BY-B_btgYRB.js +0 -1
- package/web/dist/assets/index-CI2KfVSr.css +0 -10
- package/web/dist/assets/index-DQcpggFU.js +0 -275
- package/web/dist/assets/stateDiagram-v2-MP3YSRHH-DYwe_2mB.js +0 -1
- package/web/dist/assets/swimlanesDiagram-VR7AAH4N-52Y8tgRV.js +0 -8
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -40,6 +40,18 @@ pi-harness --port 9000 --cwd /path/to/project
|
|
|
40
40
|
- 中文和英文界面、声音提醒、可选插件及 pi 扩展。
|
|
41
41
|
- CLI、开机自启服务、Docker 与 Electron 桌面版。
|
|
42
42
|
|
|
43
|
+
## 文档转换与 OKF Wiki
|
|
44
|
+
|
|
45
|
+
内置 PDF 转 Markdown 扩展支持扫描件 OCR、表格、代码和来源定位;OKF 扩展让当前 Agent 整理 PDF、Markdown、文本和 Office 文件,提取候选知识、核对重复与冲突,再生成带原文证据的 OKF Wiki。结果可在现有文档工作台中打开。
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
/pdf-md setup
|
|
49
|
+
/pdf-md convert ./raw/技术说明.pdf
|
|
50
|
+
/okf ingest ./raw,输出到 ./knowledge
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
PDF 与 Office 解析需要单独的本机 Python 3.12 环境,首次运行 `setup` 会安装依赖并下载模型;可用 `/pdf-md cancel` 取消、`/pdf-md doctor` 检查。MD/TXT 不需要此环境。知识整理使用当前会话选定的模型,疑点与冲突保留草稿,原文快照随知识目录保存。设置、恢复与资料移交见 [文档知识扩展说明](https://github.com/youweichen0208/pi-harness/blob/develop/docs/architecture-document-knowledge.md)。
|
|
54
|
+
|
|
43
55
|
## 运行与部署
|
|
44
56
|
|
|
45
57
|
```bash
|
|
@@ -68,6 +80,8 @@ npm run test:smoke
|
|
|
68
80
|
|
|
69
81
|
pi-harness is an independently maintained web and Electron interface for the pi coding agent SDK. It includes chat, file browsing and editing, attachments, a local terminal, model management, and a built-in SSH node workbench with grouped hosts, multiple PTY tabs, SFTP files, and a separate Agent conversation for each node.
|
|
70
82
|
|
|
83
|
+
Built-in document extensions provide local PDF-to-Markdown conversion and an Agent-driven OKF Wiki workflow for PDF, Markdown, text and Office files. Run `/pdf-md setup` to prepare the separate local parser, `/pdf-md convert <path>` to convert a PDF, or `/okf ingest <directory>` to organize sourced knowledge with the current conversation's model. See the [document extensions guide](https://github.com/youweichen0208/pi-harness/blob/develop/docs/architecture-document-knowledge.md).
|
|
84
|
+
|
|
71
85
|
Requires Node.js **>= 22.19.0** and a configured pi model provider. Install with `npm install -g @youweichen/pi-harness`, then run `pi-harness`. See the [SSH workbench guide](https://github.com/youweichen0208/pi-harness/blob/develop/docs/ssh-workbench.md), [Xshell guide](https://github.com/youweichen0208/pi-harness/blob/develop/docs/xshell.md), and [deployment guide](https://github.com/youweichen0208/pi-harness/blob/develop/docs/deployment.md).
|
|
72
86
|
|
|
73
87
|
## License
|
|
@@ -504,7 +504,7 @@ export class ClientSession {
|
|
|
504
504
|
* (the model choice is client-wide), so later conversations reuse the
|
|
505
505
|
* instance created with the first one.
|
|
506
506
|
*/
|
|
507
|
-
makeRuntimeFactory() {
|
|
507
|
+
makeRuntimeFactory(refreshResources = false, state) {
|
|
508
508
|
return async ({ cwd, sessionManager }) => {
|
|
509
509
|
const services = await createAgentSessionServices({
|
|
510
510
|
cwd,
|
|
@@ -513,7 +513,12 @@ export class ClientSession {
|
|
|
513
513
|
settingsManager: conversationSettings(cwd, this.agentDir),
|
|
514
514
|
resourceLoaderOptions: { extensionFactories: nativeToolExtensions() },
|
|
515
515
|
});
|
|
516
|
-
|
|
516
|
+
// A second native reload invalidates the process-wide extension factory cache.
|
|
517
|
+
if (refreshResources) {
|
|
518
|
+
await services.resourceLoader.reload();
|
|
519
|
+
refreshResources = false;
|
|
520
|
+
}
|
|
521
|
+
const created = await createAgentSessionFromServices({ services, sessionManager, model: state?.model, thinkingLevel: state?.thinkingLevel });
|
|
517
522
|
return { ...created, services, diagnostics: services.diagnostics };
|
|
518
523
|
};
|
|
519
524
|
}
|
|
@@ -1366,6 +1371,7 @@ export class ClientSession {
|
|
|
1366
1371
|
cwd: () => this.cwd,
|
|
1367
1372
|
getSession: () => this.session,
|
|
1368
1373
|
startNewSession: () => this.startNewSession(),
|
|
1374
|
+
restartSession: (conversationId) => this.restartSession(conversationId),
|
|
1369
1375
|
treeCommand: async (name, args, context) => {
|
|
1370
1376
|
const conv = this.conv;
|
|
1371
1377
|
if (conv.id !== context.conversationId)
|
|
@@ -1523,13 +1529,17 @@ export class ClientSession {
|
|
|
1523
1529
|
onAccepted?.(ok);
|
|
1524
1530
|
};
|
|
1525
1531
|
try {
|
|
1532
|
+
if (this.restartingSession)
|
|
1533
|
+
throw new Error("当前会话正在重启,请稍后重试。");
|
|
1526
1534
|
if (!["tree", "fork"].includes(parseSlash(text)?.name ?? ""))
|
|
1527
1535
|
await conv.tree?.waitForVerification();
|
|
1528
1536
|
const s = conv.session;
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1537
|
+
if (parseSlash(text)?.name !== "restart") {
|
|
1538
|
+
await flushPromptReload(s);
|
|
1539
|
+
const promptReloadError = promptReloadStatus(s).reloadError;
|
|
1540
|
+
if (promptReloadError)
|
|
1541
|
+
throw new Error(`Native prompt reload failed: ${promptReloadError}`);
|
|
1542
|
+
}
|
|
1533
1543
|
if (this.conv !== conv || conv.session !== s)
|
|
1534
1544
|
throw new Error("Conversation changed");
|
|
1535
1545
|
const extensionCommand = text.startsWith("/") && s.extensionRunner.getCommand(text.slice(1).split(" ", 1)[0]);
|
|
@@ -1841,8 +1851,59 @@ export class ClientSession {
|
|
|
1841
1851
|
});
|
|
1842
1852
|
}
|
|
1843
1853
|
}
|
|
1854
|
+
restartingSession = false;
|
|
1855
|
+
/** Recreate only this conversation's native runtime; retain its exact branch and PTYs. */
|
|
1856
|
+
async restartSession(conversationId) {
|
|
1857
|
+
const conv = this.conv;
|
|
1858
|
+
if (conv.id !== conversationId || this.switchingWorkspace || this.creatingConversation)
|
|
1859
|
+
throw new Error("对话正在切换或重启,请稍后重试。");
|
|
1860
|
+
if (this.quiesceBlocked())
|
|
1861
|
+
throw new Error("服务暂停接收新操作。");
|
|
1862
|
+
conv.tree?.assertWritable();
|
|
1863
|
+
if (!conv.session.isIdle || isRecovering(conv.recovery))
|
|
1864
|
+
throw new Error("请先停止当前任务,再重启会话。");
|
|
1865
|
+
this.restartingSession = true;
|
|
1866
|
+
let replacement;
|
|
1867
|
+
try {
|
|
1868
|
+
const previous = conv.runtime, model = conv.session.model, thinking = conv.session.thinkingLevel;
|
|
1869
|
+
replacement = await createAgentSessionRuntime(this.makeRuntimeFactory(true, { model, thinkingLevel: thinking }), {
|
|
1870
|
+
cwd: conv.cwd, agentDir: this.agentDir, sessionManager: conv.session.sessionManager,
|
|
1871
|
+
});
|
|
1872
|
+
if (this.disposed || this.conv !== conv)
|
|
1873
|
+
throw new Error("对话已切换。");
|
|
1874
|
+
await previous.dispose();
|
|
1875
|
+
conv.unsubscribe?.();
|
|
1876
|
+
conv.unsubscribe = undefined;
|
|
1877
|
+
conv.tree?.dispose();
|
|
1878
|
+
conv.tree = undefined;
|
|
1879
|
+
conv.runtime = replacement;
|
|
1880
|
+
replacement = undefined;
|
|
1881
|
+
conv.recovery = {};
|
|
1882
|
+
conv.treeProjectionRevision = undefined;
|
|
1883
|
+
conv.uiMessageCache.clear();
|
|
1884
|
+
conv.lastMessagesSig = "";
|
|
1885
|
+
await this.bindSession(conv);
|
|
1886
|
+
await this.pushSlashCommands();
|
|
1887
|
+
this.pushSettings();
|
|
1888
|
+
this.emitConversations();
|
|
1889
|
+
this.flushSnapshot(true);
|
|
1890
|
+
this.emit({ type: "notice", conversationId: conv.id, level: "info", text: "当前会话已重启,历史记录已保留。" });
|
|
1891
|
+
}
|
|
1892
|
+
finally {
|
|
1893
|
+
try {
|
|
1894
|
+
await replacement?.dispose();
|
|
1895
|
+
}
|
|
1896
|
+
finally {
|
|
1897
|
+
this.restartingSession = false;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1844
1901
|
/** /new delegates session creation and history persistence to the SDK; the Web slot is reused. */
|
|
1845
1902
|
async startNewSession() {
|
|
1903
|
+
if (this.restartingSession) {
|
|
1904
|
+
this.emitNotice("warning", "当前会话正在重启,请稍后重试。");
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1846
1907
|
if (this.conv.tree?.busy) {
|
|
1847
1908
|
this.emitNotice("warning", "等待当前切换完成。");
|
|
1848
1909
|
return;
|
|
@@ -1876,6 +1937,10 @@ export class ClientSession {
|
|
|
1876
1937
|
void this.pushSlashCommands();
|
|
1877
1938
|
}
|
|
1878
1939
|
async newChat(fresh = false, wiki = false) {
|
|
1940
|
+
if (this.restartingSession) {
|
|
1941
|
+
this.emitNotice("warning", "当前会话正在重启,请稍后重试。");
|
|
1942
|
+
return false;
|
|
1943
|
+
}
|
|
1879
1944
|
if (this.conv.tree?.busy) {
|
|
1880
1945
|
this.emitNotice("warning", "等待当前切换完成。");
|
|
1881
1946
|
return false;
|
|
@@ -2058,6 +2123,10 @@ export class ClientSession {
|
|
|
2058
2123
|
}
|
|
2059
2124
|
/** Switch the ACTIVE conversation without interrupting any other chat. */
|
|
2060
2125
|
async switchConversation(id) {
|
|
2126
|
+
if (this.restartingSession) {
|
|
2127
|
+
this.emitNotice("warning", "当前会话正在重启,请稍后重试。");
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2061
2130
|
if (this.conv.tree?.busy) {
|
|
2062
2131
|
this.emitNotice("warning", "等待当前切换完成。");
|
|
2063
2132
|
return;
|
|
@@ -2286,6 +2355,10 @@ export class ClientSession {
|
|
|
2286
2355
|
* user opened history while it was streaming.
|
|
2287
2356
|
*/
|
|
2288
2357
|
async switchSession(path) {
|
|
2358
|
+
if (this.restartingSession) {
|
|
2359
|
+
this.emitNotice("warning", "当前会话正在重启,请稍后重试。");
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2289
2362
|
if (this.conv.tree?.busy) {
|
|
2290
2363
|
this.emitNotice("warning", "等待当前切换完成。");
|
|
2291
2364
|
return;
|
|
@@ -2553,8 +2626,15 @@ export class ClientSession {
|
|
|
2553
2626
|
}
|
|
2554
2627
|
cwdQueue = null;
|
|
2555
2628
|
cwdSwitchRunning = false;
|
|
2556
|
-
get switchingWorkspace() { return this.cwdSwitchRunning; }
|
|
2629
|
+
get switchingWorkspace() { return this.cwdSwitchRunning || this.restartingSession; }
|
|
2557
2630
|
async setCwd(path, id, source) {
|
|
2631
|
+
if (this.restartingSession) {
|
|
2632
|
+
if (id)
|
|
2633
|
+
this.emit({ type: "cwd_result", requestId: id, cwd: this.cwd, ok: false, error: "当前会话正在重启,请稍后重试。" });
|
|
2634
|
+
else
|
|
2635
|
+
this.emitNotice("warning", "当前会话正在重启,请稍后重试。");
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2558
2638
|
if (this.conv.tree?.busy) {
|
|
2559
2639
|
this.emitNotice("warning", "等待当前切换完成。");
|
|
2560
2640
|
return;
|
|
@@ -87,6 +87,7 @@ export function startControlServer(opts) {
|
|
|
87
87
|
switch (req.cmd) {
|
|
88
88
|
case "status":
|
|
89
89
|
resp = { ok: true, ...service.serviceStatus() };
|
|
90
|
+
resp.activeConversations = (resp.activeConversations ?? 0) + (opts.additionalActiveConversations?.() ?? 0);
|
|
90
91
|
break;
|
|
91
92
|
case "quiesce":
|
|
92
93
|
service.quiesce();
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import { convertDocument } from "./service.js";
|
|
6
|
+
import { doctorRuntime, setupRuntime } from "./runtime.js";
|
|
7
|
+
import { getDocumentSettings, updateDocumentSettings } from "./settings.js";
|
|
8
|
+
const pathParameter = Type.String({ minLength: 1, maxLength: 4096 });
|
|
9
|
+
let activeSetup;
|
|
10
|
+
export const pdfMarkdownParameters = Type.Object({
|
|
11
|
+
inputPath: pathParameter,
|
|
12
|
+
outputDir: Type.Optional(Type.String({ minLength: 1, maxLength: 4096, description: "Output directory inside the current workspace. Defaults to converted/<filename>-<path hash>. Existing edited files are never overwritten." })),
|
|
13
|
+
});
|
|
14
|
+
export const pdfMarkdownOutput = Type.Object({
|
|
15
|
+
status: Type.Union([Type.Literal("complete"), Type.Literal("partial")]),
|
|
16
|
+
markdownPath: Type.String(), structurePath: Type.String(), sourceMapPath: Type.String(), assetsDir: Type.String(),
|
|
17
|
+
sourceHash: Type.String(), parserVersion: Type.String(), warnings: Type.Array(Type.String()),
|
|
18
|
+
});
|
|
19
|
+
/** Output files must remain accessible through the workspace's existing file UI. */
|
|
20
|
+
async function outputDirectory(cwd, input, requested) {
|
|
21
|
+
const root = await realpath(cwd);
|
|
22
|
+
const hash = createHash("sha256").update(input).digest("hex").slice(0, 8);
|
|
23
|
+
const destination = resolve(root, requested ?? `converted/${basename(input, extname(input))}-${hash}`);
|
|
24
|
+
const within = (target) => { const rel = relative(root, target); return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel); };
|
|
25
|
+
if (!within(destination) || destination === root)
|
|
26
|
+
throw Error("Choose an output directory inside the current workspace.");
|
|
27
|
+
let ancestor = destination;
|
|
28
|
+
while (true) {
|
|
29
|
+
try {
|
|
30
|
+
await lstat(ancestor);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code !== "ENOENT")
|
|
34
|
+
throw error;
|
|
35
|
+
const parent = dirname(ancestor);
|
|
36
|
+
if (parent === ancestor)
|
|
37
|
+
throw error;
|
|
38
|
+
ancestor = parent;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
// A dangling symlink exists according to lstat: its failed realpath must not
|
|
42
|
+
// be mistaken for a missing directory and skipped to the parent.
|
|
43
|
+
if (!within(await realpath(ancestor)) || !(await stat(ancestor)).isDirectory())
|
|
44
|
+
throw Error("Output directory leaves the workspace or is not a directory.");
|
|
45
|
+
return destination;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function pdfSettings(args, ctx) {
|
|
49
|
+
if (!ctx.isIdle()) {
|
|
50
|
+
ctx.ui.notify("请等待当前任务结束后修改扩展设置。", "warning");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const current = getDocumentSettings();
|
|
54
|
+
let action = args.trim();
|
|
55
|
+
if (!action && ctx.hasUI)
|
|
56
|
+
action = await ctx.ui.select("PDF 转 Markdown 设置", ["启用", "停用", "Python 路径"]) ?? "";
|
|
57
|
+
if (["on", "启用"].includes(action))
|
|
58
|
+
await updateDocumentSettings({ pdfEnabled: true });
|
|
59
|
+
else if (["off", "停用"].includes(action))
|
|
60
|
+
await updateDocumentSettings({ pdfEnabled: false });
|
|
61
|
+
else if (action === "Python 路径" || action.startsWith("python ")) {
|
|
62
|
+
const path = action.startsWith("python ") ? action.slice(7).trim() : await ctx.ui.input("Python 可执行文件路径(留空恢复自动发现)", current.pythonPath ?? "");
|
|
63
|
+
if (path === undefined)
|
|
64
|
+
return;
|
|
65
|
+
await updateDocumentSettings({ pythonPath: path.trim() || undefined });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
ctx.ui.notify(`PDF 转 Markdown:${current.pdfEnabled ? "启用" : "停用"};Python:${current.pythonPath ?? "自动发现"}。用 /pdf-md settings on|off|python <路径> 修改。`, "info");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
ctx.ui.notify("设置已保存。请使用 /reload 或新建会话更新工具列表;停用后现有工具调用会立即被拒绝。", "info");
|
|
72
|
+
}
|
|
73
|
+
export const createPdfMarkdownExtension = () => pi => {
|
|
74
|
+
pi.registerTool({
|
|
75
|
+
name: "pdf_to_markdown", label: "PDF to Markdown",
|
|
76
|
+
description: "Convert a local PDF, including scans, to structured Markdown with local Docling OCR, tables, code, images and source locations. Uses the local parser without extra chat model calls. Returns file paths and quality warnings. Read the Markdown and warnings before reporting quality; do not invent missing content. Link the resulting workspace-relative Markdown file in your final answer.",
|
|
77
|
+
exposure: getDocumentSettings().pdfEnabled ? "deferred" : "hidden", executionMode: "sequential",
|
|
78
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
79
|
+
parameters: pdfMarkdownParameters, outputSchema: pdfMarkdownOutput,
|
|
80
|
+
async execute(_id, args, signal, onUpdate, ctx) {
|
|
81
|
+
if (!getDocumentSettings().pdfEnabled)
|
|
82
|
+
throw Error("PDF conversion is disabled. Enable it with /pdf-md settings on.");
|
|
83
|
+
signal?.throwIfAborted();
|
|
84
|
+
const cwd = await realpath(ctx.cwd);
|
|
85
|
+
const inputPath = await realpath(resolve(cwd, args.inputPath));
|
|
86
|
+
if (extname(inputPath).toLowerCase() !== ".pdf")
|
|
87
|
+
throw Error("pdf_to_markdown accepts PDF files; use okf_ingest for other document formats.");
|
|
88
|
+
const outputDir = await outputDirectory(cwd, inputPath, args.outputDir);
|
|
89
|
+
const result = await convertDocument({ inputPath, outputDir, signal, onProgress: text => onUpdate?.({ content: [{ type: "text", text }], details: {} }) });
|
|
90
|
+
const localPath = (path) => relative(cwd, path).split(sep).join("/");
|
|
91
|
+
const data = { status: result.status, markdownPath: localPath(result.markdownPath), structurePath: localPath(result.structurePath), sourceMapPath: localPath(result.sourceMapPath), assetsDir: localPath(result.assetsDir), sourceHash: result.sourceHash, parserVersion: result.parserVersion, warnings: result.warnings };
|
|
92
|
+
return { content: [{ type: "text", text: `${JSON.stringify(data, null, 2)}\nRead the generated Markdown and conversion warnings. In your final reply, link [converted document](${data.markdownPath}).` }], structuredContent: data, details: {} };
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
pi.registerCommand("pdf-md", {
|
|
96
|
+
description: "PDF 转 Markdown;convert、setup、cancel、doctor、settings",
|
|
97
|
+
async handler(args, ctx) {
|
|
98
|
+
const [, action = "", rest = ""] = /^(\S+)?\s*([\s\S]*)$/.exec(args.trim()) ?? [];
|
|
99
|
+
if (action === "settings") {
|
|
100
|
+
await pdfSettings(rest, ctx);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (action === "cancel") {
|
|
104
|
+
if (!activeSetup) {
|
|
105
|
+
ctx.ui.notify("没有正在安装的文档解析环境。", "info");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (activeSetup.ownerSessionId !== ctx.sessionManager.getSessionId()) {
|
|
109
|
+
ctx.ui.notify("解析环境正在其他会话中安装,请在发起安装的会话中运行 /pdf-md cancel。", "warning");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
activeSetup.controller.abort();
|
|
113
|
+
ctx.ui.notify("已请求取消解析环境安装,正在清理安装进程。", "info");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (action === "doctor" || action === "setup") {
|
|
117
|
+
if (!ctx.isIdle()) {
|
|
118
|
+
ctx.ui.notify("请等待当前任务结束后检查或安装解析环境。", "warning");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (activeSetup) {
|
|
122
|
+
ctx.ui.notify("文档解析环境已经在安装中。可在发起安装的会话运行 /pdf-md cancel 取消。", "warning");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const setup = action === "setup" ? { ownerSessionId: ctx.sessionManager.getSessionId(), controller: new AbortController() } : undefined;
|
|
126
|
+
if (setup)
|
|
127
|
+
activeSetup = setup;
|
|
128
|
+
const run = async () => {
|
|
129
|
+
try {
|
|
130
|
+
const signal = setup ? (ctx.signal ? AbortSignal.any([setup.controller.signal, ctx.signal]) : setup.controller.signal) : ctx.signal;
|
|
131
|
+
const result = setup ? await setupRuntime({ signal, onProgress: text => ctx.ui.setStatus("document-runtime", text) }) : await doctorRuntime({ signal });
|
|
132
|
+
ctx.ui.notify(JSON.stringify(result, null, 2), result.ready ? "info" : "warning");
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
ctx.ui.notify(error.message, "error");
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
if (setup && activeSetup === setup)
|
|
139
|
+
activeSetup = undefined;
|
|
140
|
+
ctx.ui.setStatus("document-runtime", undefined);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
if (setup) {
|
|
144
|
+
// Acknowledge this explicit command immediately so the same WebUI
|
|
145
|
+
// input remains available for /pdf-md cancel during installation.
|
|
146
|
+
ctx.ui.notify("已开始安装文档解析环境。进度显示在状态栏;可用 /pdf-md cancel 取消。", "info");
|
|
147
|
+
setup.task = run();
|
|
148
|
+
}
|
|
149
|
+
else
|
|
150
|
+
await run();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (action !== "convert" || !rest) {
|
|
154
|
+
ctx.ui.notify("用法:/pdf-md convert <PDF 路径及可选输出要求>;/pdf-md setup;/pdf-md cancel;/pdf-md doctor;/pdf-md settings", "info");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!getDocumentSettings().pdfEnabled) {
|
|
158
|
+
ctx.ui.notify("PDF 转换已停用,请先用 /pdf-md settings on 启用。", "warning");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
pi.sendUserMessage(`Convert the PDF described below with the native pdf_to_markdown tool. Use tool_search to discover it if needed. Preserve the source document, inspect the conversion warnings, and give a clickable relative link to the resulting Markdown. Do not install dependencies unless I explicitly request setup.\n\n${rest}`, ctx.isIdle() ? undefined : { deliverAs: "followUp" });
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
};
|