@actalk/inkos-studio 1.3.12 → 1.4.0-canary.43.1
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/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +368 -6
- package/dist/api/server.js.map +1 -1
- package/dist/assets/{_baseUniq-BkDywaR2.js → _baseUniq-DuJFcOdr.js} +1 -1
- package/dist/assets/{arc-CKcvYwZr.js → arc-CRu6VUhj.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-DcjXLG7B.js → architectureDiagram-Q4EWVU46-CIqV4qz7.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-Dtg0MQfu.js → blockDiagram-DXYQGD6D-fTi_ECOn.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-BJe7RXHv.js → c4Diagram-AHTNJAMY-Bzk5MYNH.js} +1 -1
- package/dist/assets/channel-uCIAc1hE.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-DFmjmdUg.js → chunk-4BX2VUAB-HjzLE1LZ.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-jN_VWqpy.js → chunk-4TB4RGXK-BJyVLr8h.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-B3hhvAAx.js → chunk-55IACEB6-CFMufWYu.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-BNAX6ecW.js → chunk-EDXVE4YY-BsY-2qu7.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-C5CE5GLy.js → chunk-FMBD7UC4-CIhpQnBe.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-BUbNlQKM.js → chunk-OYMX7WX6-CIZ6okx7.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-BPYnXnSG.js → chunk-QZHKN3VN-Dkcne4nr.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-6jL2GB9I.js → chunk-YZCP3GAM-DQuvXxIP.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-B8poauXH.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-B8poauXH.js +1 -0
- package/dist/assets/clone-CXmG6BfO.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-D-CqlNDA.js → cose-bilkent-S5V4N54A-CT-CUv0U.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-DUv2RDEw.js → dagre-KV5264BT-DRS18Avn.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DoJ-lofw.js → diagram-5BDNPKRD-CI3ev01f.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-D9Ag3zpJ.js → diagram-G4DWMVQ6-B1QvOXJt.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-B5hJ7Jq2.js → diagram-MMDJMWI5-DJIeBvHf.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-dJ4_8Wah.js → diagram-TYMM5635-DKSFwm0R.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-D7mVQxRP.js → erDiagram-SMLLAGMA-D0i9HWsj.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-DImypBUn.js → flowDiagram-DWJPFMVM-Dq4sOV0a.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-CDEIYHYF.js → ganttDiagram-T4ZO3ILL-FVlGz1Cc.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-BERSAygy.js → gitGraphDiagram-UUTBAWPF-jVSM4fIZ.js} +1 -1
- package/dist/assets/{graph-C9kyvgMF.js → graph-BwgR_wTK.js} +1 -1
- package/dist/assets/{highlighted-body-OFNGDK62-C8cjOTsw.js → highlighted-body-OFNGDK62-ht7p7AB1.js} +1 -1
- package/dist/assets/index-BWQL1zpM.css +1 -0
- package/dist/assets/{index-CwOLa5XO.js → index-COgOkP4B.js} +271 -265
- package/dist/assets/{infoDiagram-42DDH7IO-CS1g0dIg.js → infoDiagram-42DDH7IO-D4nEy-tx.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-CT26U9K1.js → ishikawaDiagram-UXIWVN3A-BY35FcPr.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-BhrnuEWH.js → journeyDiagram-VCZTEJTY-C2PT0pXA.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-Cgbe3XsR.js → kanban-definition-6JOO6SKY-DD0dYiJ-.js} +1 -1
- package/dist/assets/{layout-BIZFnXJR.js → layout-sC_Hhgm4.js} +1 -1
- package/dist/assets/{linear-DopjI4nA.js → linear-0i0GyJQs.js} +1 -1
- package/dist/assets/{min-pvZVDxuq.js → min-DOL6Y_RU.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-BtWuqxHq.js → mindmap-definition-QFDTVHPH-MEQTU3lB.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-DOQJ2nJ_.js → pieDiagram-DEJITSTG-eg09tT0v.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-C8OgY4a1.js → quadrantDiagram-34T5L4WZ-Dbp-ydjf.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-B6dUv_A2.js → requirementDiagram-MS252O5E-Bxfwqnat.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-D1hhsStt.js → sankeyDiagram-XADWPNL6-BcZnxYY3.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-BY3uaAnp.js → sequenceDiagram-FGHM5R23-poDxcRyl.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-BY0S48rv.js → stateDiagram-FHFEXIEX-yNt0zNcA.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-CVrFbbZA.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-D5kS335m.js → timeline-definition-GMOUNBTQ-C7rYgB33.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-IdJoj8td.js → vennDiagram-DHZGUBPP-BWbCUy6T.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-BGYQy4tS.js → wardley-RL74JXVD-CR0zTOY_.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-C2MOgjwO.js → wardleyDiagram-NUSXRM2D-zhLoptAy.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-CAywHFqp.js → xychartDiagram-5P7HB3ND-N0gnUPD3.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/dist/assets/channel-rGDv8Aeg.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-D44TwGCR.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-D44TwGCR.js +0 -1
- package/dist/assets/clone-6v8XJWol.js +0 -1
- package/dist/assets/index-CorDERH5.css +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-Cz1ZHG9i.js +0 -1
package/dist/api/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,EAyCL,KAAK,aAAa,EAGnB,MAAM,oBAAoB,CAAC;AAyoC5B,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,8EAi2E5E;AAID,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,SAAO,EACX,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAAC,IAAI,CAAC,CA8Cf"}
|
package/dist/api/server.js
CHANGED
|
@@ -2,8 +2,8 @@ import { Hono } from "hono";
|
|
|
2
2
|
import { cors } from "hono/cors";
|
|
3
3
|
import { streamSSE } from "hono/streaming";
|
|
4
4
|
import { serve } from "@hono/node-server";
|
|
5
|
-
import { StateManager, PipelineRunner, createLLMClient, createLogger, createInteractionToolsFromDeps, computeAnalytics, loadProjectConfig, loadProjectSession, processProjectInteractionRequest, resolveSessionActiveBook, listBookSessions, loadBookSession, appendManualSessionMessages, createAndPersistBookSession, renameBookSession, deleteBookSession, migrateBookSession, SessionAlreadyMigratedError, runAgentSession, buildAgentSystemPrompt, resolveServicePreset, resolveServiceProviderFamily, resolveServiceModelsBaseUrl, resolveServiceModel, loadSecrets, saveSecrets, listModelsForService, isApiKeyOptionalForEndpoint, getAllEndpoints, probeModelsFromUpstream, fetchWithProxy, chatCompletion, buildExportArtifact, GLOBAL_ENV_PATH, Scheduler, } from "@actalk/inkos-core";
|
|
6
|
-
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { StateManager, PipelineRunner, createLLMClient, createLogger, createInteractionToolsFromDeps, computeAnalytics, loadProjectConfig, loadProjectSession, processProjectInteractionRequest, resolveSessionActiveBook, listBookSessions, loadBookSession, appendManualSessionMessages, createAndPersistBookSession, renameBookSession, deleteBookSession, migrateBookSession, SessionAlreadyMigratedError, runAgentSession, buildAgentSystemPrompt, resolveServicePreset, resolveServiceProviderFamily, resolveServiceModelsBaseUrl, resolveServiceModel, loadSecrets, saveSecrets, listModelsForService, isApiKeyOptionalForEndpoint, getAllEndpoints, probeModelsFromUpstream, fetchWithProxy, chatCompletion, buildExportArtifact, GLOBAL_ENV_PATH, COVER_PROVIDER_PRESETS, Scheduler, coverSecretKey, resolveCoverProviderPreset, } from "@actalk/inkos-core";
|
|
6
|
+
import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
7
7
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
8
8
|
import { isSafeBookId } from "./safety.js";
|
|
9
9
|
import { ApiError } from "./errors.js";
|
|
@@ -31,6 +31,8 @@ const AGENT_LABELS = {
|
|
|
31
31
|
};
|
|
32
32
|
const TOOL_LABELS = {
|
|
33
33
|
read: "读取文件", edit: "编辑文件", grep: "搜索", ls: "列目录",
|
|
34
|
+
short_fiction_run: "短篇生产",
|
|
35
|
+
generate_cover: "生成封面",
|
|
34
36
|
};
|
|
35
37
|
function resolveToolLabel(tool, agent) {
|
|
36
38
|
if (tool === "sub_agent" && agent)
|
|
@@ -50,10 +52,12 @@ function summarizeResult(result) {
|
|
|
50
52
|
return String(result).slice(0, 200);
|
|
51
53
|
}
|
|
52
54
|
function compareServiceListItems(left, right) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
const priority = ["kkaiapi", "openrouter", "newapi", "siliconcloud"];
|
|
56
|
+
const leftPriority = priority.indexOf(left.service);
|
|
57
|
+
const rightPriority = priority.indexOf(right.service);
|
|
58
|
+
if (leftPriority !== -1 || rightPriority !== -1) {
|
|
59
|
+
return (leftPriority === -1 ? 999 : leftPriority) - (rightPriority === -1 ? 999 : rightPriority);
|
|
60
|
+
}
|
|
57
61
|
return 0;
|
|
58
62
|
}
|
|
59
63
|
function isHeaderSafeApiKey(value) {
|
|
@@ -117,6 +121,41 @@ function extractToolError(result) {
|
|
|
117
121
|
}
|
|
118
122
|
return String(result).slice(0, 500);
|
|
119
123
|
}
|
|
124
|
+
function resolveProjectImageFile(root, rawPath) {
|
|
125
|
+
let relPath;
|
|
126
|
+
try {
|
|
127
|
+
relPath = decodeURIComponent(rawPath).replace(/^\/+/u, "");
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new ApiError(400, "INVALID_PROJECT_FILE_PATH", "Invalid project file path");
|
|
131
|
+
}
|
|
132
|
+
if (!relPath
|
|
133
|
+
|| relPath.includes("\0")
|
|
134
|
+
|| isAbsolute(relPath)
|
|
135
|
+
|| relPath.split(/[\\/]+/u).includes("..")) {
|
|
136
|
+
throw new ApiError(400, "INVALID_PROJECT_FILE_PATH", "Invalid project file path");
|
|
137
|
+
}
|
|
138
|
+
if (!relPath.startsWith("shorts/") && !relPath.startsWith("covers/")) {
|
|
139
|
+
throw new ApiError(400, "INVALID_PROJECT_FILE_PATH", "Only generated shorts/ and covers/ images can be previewed");
|
|
140
|
+
}
|
|
141
|
+
const ext = relPath.split(".").pop()?.toLowerCase() ?? "";
|
|
142
|
+
const contentTypes = {
|
|
143
|
+
png: "image/png",
|
|
144
|
+
jpg: "image/jpeg",
|
|
145
|
+
jpeg: "image/jpeg",
|
|
146
|
+
webp: "image/webp",
|
|
147
|
+
};
|
|
148
|
+
const contentType = contentTypes[ext];
|
|
149
|
+
if (!contentType) {
|
|
150
|
+
throw new ApiError(415, "UNSUPPORTED_PROJECT_FILE_TYPE", "Unsupported project file type");
|
|
151
|
+
}
|
|
152
|
+
const resolved = resolve(root, relPath);
|
|
153
|
+
const rel = relative(root, resolved);
|
|
154
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
|
|
155
|
+
throw new ApiError(400, "INVALID_PROJECT_FILE_PATH", "Invalid project file path");
|
|
156
|
+
}
|
|
157
|
+
return { resolved, contentType };
|
|
158
|
+
}
|
|
120
159
|
function isLikelyFailedToolResult(exec) {
|
|
121
160
|
if (exec.status === "error")
|
|
122
161
|
return true;
|
|
@@ -134,6 +173,175 @@ function isWriteNextInstruction(instruction) {
|
|
|
134
173
|
return /^(continue|继续|继续写|写下一章|write next|下一章|再来一章)$/i.test(trimmed)
|
|
135
174
|
|| /(继续写|写下一章|下一章|再来一章|write\s+next)/i.test(trimmed);
|
|
136
175
|
}
|
|
176
|
+
const CHAT_EDIT_WARNING = "[warning] Chat external edit requires review before continuation.";
|
|
177
|
+
const CHAT_EDIT_TEXT_EXTENSIONS = /\.(md|txt|json|ya?ml)$/i;
|
|
178
|
+
const CHAT_EDIT_ALLOWED_ROOTS = new Set(["books", "shorts", "covers", "genres"]);
|
|
179
|
+
function parseReplacementInstruction(instruction) {
|
|
180
|
+
const inFileQuoted = instruction.match(/(?:里|里的|中|中的|里面)\s*[「“"]([\s\S]+?)[」”"]\s*(?:改成|替换成|换成)\s*[「“"]([\s\S]+?)[」”"]/);
|
|
181
|
+
if (inFileQuoted?.[1] && inFileQuoted[2] !== undefined) {
|
|
182
|
+
return { oldText: inFileQuoted[1], newText: inFileQuoted[2] };
|
|
183
|
+
}
|
|
184
|
+
const quoted = instruction.match(/(?:把|将)\s*[「“"]([\s\S]+?)[」”"]\s*(?:改成|替换成|换成)\s*[「“"]([\s\S]+?)[」”"]/);
|
|
185
|
+
if (quoted?.[1] && quoted[2] !== undefined) {
|
|
186
|
+
return { oldText: quoted[1], newText: quoted[2] };
|
|
187
|
+
}
|
|
188
|
+
const plain = instruction.match(/(?:把|将)\s+([^\s,。;;]+)\s*(?:改成|替换成|换成)\s+([^\n,。;;]+)/);
|
|
189
|
+
if (plain?.[1] && plain[2] !== undefined) {
|
|
190
|
+
return { oldText: plain[1], newText: plain[2].trim() };
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
function parseChapterNumberForEdit(instruction) {
|
|
195
|
+
const match = instruction.match(/第\s*(\d{1,4})\s*章/);
|
|
196
|
+
if (!match?.[1])
|
|
197
|
+
return null;
|
|
198
|
+
const chapterNumber = Number.parseInt(match[1], 10);
|
|
199
|
+
return Number.isInteger(chapterNumber) && chapterNumber > 0 ? chapterNumber : null;
|
|
200
|
+
}
|
|
201
|
+
function parseExplicitEditPath(instruction) {
|
|
202
|
+
const match = instruction.match(/(?:把|将)\s+([^「“"\s,。;;]+?\.[A-Za-z0-9]+)\s*(?:里|里的|中|中的|里面)/);
|
|
203
|
+
return match?.[1]?.trim() ?? null;
|
|
204
|
+
}
|
|
205
|
+
function countContentUnits(content) {
|
|
206
|
+
const stripped = content
|
|
207
|
+
.replace(/^#{1,6}\s+.*$/gm, "")
|
|
208
|
+
.trim();
|
|
209
|
+
if (!stripped)
|
|
210
|
+
return 0;
|
|
211
|
+
if (/[\u3400-\u9fff]/.test(stripped)) {
|
|
212
|
+
return stripped.replace(/\s/g, "").length;
|
|
213
|
+
}
|
|
214
|
+
return stripped.split(/\s+/).filter(Boolean).length;
|
|
215
|
+
}
|
|
216
|
+
function resolveExternalChatEditPath(root, requestedPath) {
|
|
217
|
+
if (isAbsolute(requestedPath)) {
|
|
218
|
+
throw new ApiError(400, "UNSUPPORTED_CHAT_EDIT_TARGET", "Chat external edits only support project-relative content paths.");
|
|
219
|
+
}
|
|
220
|
+
const projectRoot = resolve(root);
|
|
221
|
+
const resolved = resolve(projectRoot, requestedPath);
|
|
222
|
+
const rel = relative(projectRoot, resolved).replace(/\\/g, "/");
|
|
223
|
+
if (!rel || rel.startsWith("../") || rel === "..") {
|
|
224
|
+
throw new ApiError(400, "UNSUPPORTED_CHAT_EDIT_TARGET", "Chat external edit path escapes the project root.");
|
|
225
|
+
}
|
|
226
|
+
const first = rel.split("/")[0] ?? "";
|
|
227
|
+
if (!CHAT_EDIT_ALLOWED_ROOTS.has(first)) {
|
|
228
|
+
throw new ApiError(400, "UNSUPPORTED_CHAT_EDIT_TARGET", "Chat external edits cannot modify source code, config, or arbitrary project files.");
|
|
229
|
+
}
|
|
230
|
+
if (rel.includes("/.inkos/") || rel.endsWith("/.inkos") || rel.includes("/secrets") || rel.endsWith(".env")) {
|
|
231
|
+
throw new ApiError(400, "UNSUPPORTED_CHAT_EDIT_TARGET", "Chat external edits cannot modify secrets or runtime internals.");
|
|
232
|
+
}
|
|
233
|
+
if (!CHAT_EDIT_TEXT_EXTENSIONS.test(rel)) {
|
|
234
|
+
throw new ApiError(400, "UNSUPPORTED_CHAT_EDIT_TARGET", "Chat external edits only support text content files.");
|
|
235
|
+
}
|
|
236
|
+
return { path: resolved, rel };
|
|
237
|
+
}
|
|
238
|
+
async function findChapterFile(root, bookId, chapterNumber) {
|
|
239
|
+
const chaptersDir = join(root, "books", bookId, "chapters");
|
|
240
|
+
const padded = String(chapterNumber).padStart(4, "0");
|
|
241
|
+
const files = await readdir(chaptersDir).catch(() => []);
|
|
242
|
+
const match = files.find((file) => file.startsWith(`${padded}_`) && file.endsWith(".md"));
|
|
243
|
+
return match ? join(chaptersDir, match) : null;
|
|
244
|
+
}
|
|
245
|
+
function parseBookChapterFromRelativePath(rel) {
|
|
246
|
+
const match = rel.match(/^books\/([^/]+)\/chapters\/(\d{4})_[^/]+\.md$/);
|
|
247
|
+
if (!match?.[1] || !match[2])
|
|
248
|
+
return null;
|
|
249
|
+
const chapterNumber = Number.parseInt(match[2], 10);
|
|
250
|
+
return Number.isInteger(chapterNumber) ? { bookId: match[1], chapterNumber } : null;
|
|
251
|
+
}
|
|
252
|
+
async function syncExternalChapterEdit(params) {
|
|
253
|
+
const now = new Date().toISOString();
|
|
254
|
+
const index = [...(await params.state.loadChapterIndex(params.bookId))];
|
|
255
|
+
const updated = index.map((chapter) => chapter.number === params.chapterNumber
|
|
256
|
+
? {
|
|
257
|
+
...chapter,
|
|
258
|
+
status: "audit-failed",
|
|
259
|
+
wordCount: countContentUnits(params.content),
|
|
260
|
+
updatedAt: now,
|
|
261
|
+
auditIssues: [
|
|
262
|
+
...chapter.auditIssues.filter((issue) => issue !== CHAT_EDIT_WARNING),
|
|
263
|
+
CHAT_EDIT_WARNING,
|
|
264
|
+
],
|
|
265
|
+
}
|
|
266
|
+
: chapter);
|
|
267
|
+
if (updated.length > 0) {
|
|
268
|
+
await params.state.saveChapterIndex(params.bookId, updated);
|
|
269
|
+
}
|
|
270
|
+
const runtimeDir = join(params.root, "books", params.bookId, "story", "runtime");
|
|
271
|
+
const padded = String(params.chapterNumber).padStart(4, "0");
|
|
272
|
+
const runtimeFiles = await readdir(runtimeDir).catch(() => []);
|
|
273
|
+
await Promise.all(runtimeFiles
|
|
274
|
+
.filter((file) => file.startsWith(`chapter-${padded}.`))
|
|
275
|
+
.map((file) => rm(join(runtimeDir, file), { force: true })));
|
|
276
|
+
}
|
|
277
|
+
async function tryHandleExternalChatEdit(params) {
|
|
278
|
+
const replacement = parseReplacementInstruction(params.instruction);
|
|
279
|
+
if (!replacement)
|
|
280
|
+
return null;
|
|
281
|
+
const explicitPath = parseExplicitEditPath(params.instruction);
|
|
282
|
+
if (explicitPath) {
|
|
283
|
+
const target = resolveExternalChatEditPath(params.root, explicitPath);
|
|
284
|
+
const content = await readFile(target.path, "utf-8").catch((error) => {
|
|
285
|
+
throw new ApiError(404, "CHAT_EDIT_TARGET_NOT_FOUND", error instanceof Error ? error.message : String(error));
|
|
286
|
+
});
|
|
287
|
+
const first = content.indexOf(replacement.oldText);
|
|
288
|
+
if (first === -1) {
|
|
289
|
+
throw new ApiError(400, "EDIT_TARGET_NOT_FOUND", "要替换的原文没有在目标文件中找到。");
|
|
290
|
+
}
|
|
291
|
+
if (content.indexOf(replacement.oldText, first + replacement.oldText.length) !== -1) {
|
|
292
|
+
throw new ApiError(400, "EDIT_TARGET_AMBIGUOUS", "要替换的原文出现多次,请给出更具体的一段。");
|
|
293
|
+
}
|
|
294
|
+
const updated = content.slice(0, first) + replacement.newText + content.slice(first + replacement.oldText.length);
|
|
295
|
+
await writeFile(target.path, updated, "utf-8");
|
|
296
|
+
const chapterTarget = parseBookChapterFromRelativePath(target.rel);
|
|
297
|
+
if (chapterTarget) {
|
|
298
|
+
await syncExternalChapterEdit({
|
|
299
|
+
state: params.state,
|
|
300
|
+
root: params.root,
|
|
301
|
+
bookId: chapterTarget.bookId,
|
|
302
|
+
chapterNumber: chapterTarget.chapterNumber,
|
|
303
|
+
content: updated,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
activeBookId: chapterTarget?.bookId ?? params.activeBookId ?? undefined,
|
|
308
|
+
responseText: `已直接编辑 ${target.rel}${chapterTarget ? ",并标记为需要复核" : ""}。`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (!params.activeBookId)
|
|
312
|
+
return null;
|
|
313
|
+
const chapterNumber = parseChapterNumberForEdit(params.instruction);
|
|
314
|
+
if (!replacement || !chapterNumber)
|
|
315
|
+
return null;
|
|
316
|
+
const chapterPath = await findChapterFile(params.root, params.activeBookId, chapterNumber);
|
|
317
|
+
if (!chapterPath) {
|
|
318
|
+
throw new ApiError(404, "CHAPTER_NOT_FOUND", `Chapter ${chapterNumber} not found in ${params.activeBookId}`);
|
|
319
|
+
}
|
|
320
|
+
if (!CHAT_EDIT_TEXT_EXTENSIONS.test(chapterPath)) {
|
|
321
|
+
throw new ApiError(400, "UNSUPPORTED_EDIT_TARGET", "Chat external edits only support text files.");
|
|
322
|
+
}
|
|
323
|
+
const content = await readFile(chapterPath, "utf-8");
|
|
324
|
+
const first = content.indexOf(replacement.oldText);
|
|
325
|
+
if (first === -1) {
|
|
326
|
+
throw new ApiError(400, "EDIT_TARGET_NOT_FOUND", "要替换的原文没有在目标章节中找到。");
|
|
327
|
+
}
|
|
328
|
+
if (content.indexOf(replacement.oldText, first + replacement.oldText.length) !== -1) {
|
|
329
|
+
throw new ApiError(400, "EDIT_TARGET_AMBIGUOUS", "要替换的原文出现多次,请给出更具体的一段。");
|
|
330
|
+
}
|
|
331
|
+
const updated = content.slice(0, first) + replacement.newText + content.slice(first + replacement.oldText.length);
|
|
332
|
+
await writeFile(chapterPath, updated, "utf-8");
|
|
333
|
+
await syncExternalChapterEdit({
|
|
334
|
+
state: params.state,
|
|
335
|
+
root: params.root,
|
|
336
|
+
bookId: params.activeBookId,
|
|
337
|
+
chapterNumber,
|
|
338
|
+
content: updated,
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
activeBookId: params.activeBookId,
|
|
342
|
+
responseText: `已直接编辑 ${params.activeBookId} 第 ${chapterNumber} 章,并标记为需要复核。`,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
137
345
|
function looksLikeBookCreatedClaim(responseText) {
|
|
138
346
|
return /(?:已|已经|成功).{0,12}(?:创建|建书|初始化|保存).{0,12}(?:作品|书|书籍|文件夹)?/.test(responseText)
|
|
139
347
|
|| /\b(?:created|initiali[sz]ed|saved)\b.{0,40}\b(?:book|project|novel)\b/i.test(responseText);
|
|
@@ -267,6 +475,42 @@ function mergeServiceConfig(existing, updates) {
|
|
|
267
475
|
}
|
|
268
476
|
return [...merged.values()];
|
|
269
477
|
}
|
|
478
|
+
function normalizeCoverConfig(raw) {
|
|
479
|
+
if (!raw || typeof raw !== "object")
|
|
480
|
+
return undefined;
|
|
481
|
+
const record = raw;
|
|
482
|
+
const service = typeof record.service === "string" ? record.service : "";
|
|
483
|
+
const preset = resolveCoverProviderPreset(service);
|
|
484
|
+
if (!preset)
|
|
485
|
+
return undefined;
|
|
486
|
+
const requestedModel = typeof record.model === "string" ? record.model.trim() : "";
|
|
487
|
+
const model = requestedModel && preset.models.includes(requestedModel)
|
|
488
|
+
? requestedModel
|
|
489
|
+
: preset.defaultModel;
|
|
490
|
+
return { service: preset.service, model };
|
|
491
|
+
}
|
|
492
|
+
function syncTopLevelLlmMirror(llm) {
|
|
493
|
+
const selectedService = typeof llm.service === "string" ? llm.service : undefined;
|
|
494
|
+
if (!selectedService)
|
|
495
|
+
return;
|
|
496
|
+
const services = normalizeServiceConfig(llm.services);
|
|
497
|
+
const selectedEntry = services.find((entry) => serviceConfigKey(entry) === selectedService)
|
|
498
|
+
?? (!isCustomServiceId(selectedService) ? { service: selectedService } : undefined);
|
|
499
|
+
if (!selectedEntry)
|
|
500
|
+
return;
|
|
501
|
+
const preset = resolveServicePreset(selectedEntry.service);
|
|
502
|
+
llm.provider = resolveServiceProviderFamily(selectedEntry.service) ?? "openai";
|
|
503
|
+
llm.baseUrl = selectedEntry.baseUrl ?? preset?.baseUrl ?? "";
|
|
504
|
+
const defaultModel = typeof llm.defaultModel === "string" ? llm.defaultModel.trim() : "";
|
|
505
|
+
if (defaultModel)
|
|
506
|
+
llm.model = defaultModel;
|
|
507
|
+
if (selectedEntry.temperature !== undefined)
|
|
508
|
+
llm.temperature = selectedEntry.temperature;
|
|
509
|
+
if (selectedEntry.apiFormat !== undefined)
|
|
510
|
+
llm.apiFormat = selectedEntry.apiFormat;
|
|
511
|
+
if (selectedEntry.stream !== undefined)
|
|
512
|
+
llm.stream = selectedEntry.stream;
|
|
513
|
+
}
|
|
270
514
|
async function loadRawConfig(root) {
|
|
271
515
|
const configPath = join(root, "inkos.json");
|
|
272
516
|
const raw = await readFile(configPath, "utf-8");
|
|
@@ -1177,9 +1421,76 @@ export function createStudioServer(initialConfig, root) {
|
|
|
1177
1421
|
if (body.service !== undefined) {
|
|
1178
1422
|
llm.service = body.service;
|
|
1179
1423
|
}
|
|
1424
|
+
syncTopLevelLlmMirror(llm);
|
|
1180
1425
|
await saveRawConfig(root, config);
|
|
1181
1426
|
return c.json({ ok: true });
|
|
1182
1427
|
});
|
|
1428
|
+
app.get("/api/v1/cover/config", async (c) => {
|
|
1429
|
+
const config = await loadRawConfig(root);
|
|
1430
|
+
const llm = config.llm ?? {};
|
|
1431
|
+
const cover = normalizeCoverConfig(llm.cover);
|
|
1432
|
+
const secrets = await loadSecrets(root);
|
|
1433
|
+
return c.json({
|
|
1434
|
+
service: cover?.service ?? null,
|
|
1435
|
+
model: cover?.model ?? null,
|
|
1436
|
+
providers: COVER_PROVIDER_PRESETS.map((provider) => ({
|
|
1437
|
+
service: provider.service,
|
|
1438
|
+
label: provider.label,
|
|
1439
|
+
baseUrl: provider.baseUrl,
|
|
1440
|
+
defaultModel: provider.defaultModel,
|
|
1441
|
+
models: provider.models,
|
|
1442
|
+
connected: Boolean(secrets.services[coverSecretKey(provider.service)]?.apiKey || secrets.services[provider.service]?.apiKey),
|
|
1443
|
+
})),
|
|
1444
|
+
});
|
|
1445
|
+
});
|
|
1446
|
+
app.put("/api/v1/cover/config", async (c) => {
|
|
1447
|
+
const body = await c.req.json();
|
|
1448
|
+
const preset = resolveCoverProviderPreset(body.service);
|
|
1449
|
+
if (!preset) {
|
|
1450
|
+
return c.json({ error: "Unsupported cover service" }, 400);
|
|
1451
|
+
}
|
|
1452
|
+
const model = typeof body.model === "string" && preset.models.includes(body.model)
|
|
1453
|
+
? body.model
|
|
1454
|
+
: preset.defaultModel;
|
|
1455
|
+
const config = await loadRawConfig(root);
|
|
1456
|
+
config.llm = config.llm ?? {};
|
|
1457
|
+
const llm = config.llm;
|
|
1458
|
+
llm.cover = {
|
|
1459
|
+
service: preset.service,
|
|
1460
|
+
model,
|
|
1461
|
+
};
|
|
1462
|
+
await saveRawConfig(root, config);
|
|
1463
|
+
return c.json({ ok: true, service: preset.service, model });
|
|
1464
|
+
});
|
|
1465
|
+
app.get("/api/v1/cover/secret/:service", async (c) => {
|
|
1466
|
+
const service = c.req.param("service");
|
|
1467
|
+
if (!resolveCoverProviderPreset(service)) {
|
|
1468
|
+
return c.json({ error: "Unsupported cover service" }, 400);
|
|
1469
|
+
}
|
|
1470
|
+
const secrets = await loadSecrets(root);
|
|
1471
|
+
return c.json({ apiKey: secrets.services[coverSecretKey(service)]?.apiKey ?? "" });
|
|
1472
|
+
});
|
|
1473
|
+
app.put("/api/v1/cover/secret/:service", async (c) => {
|
|
1474
|
+
const service = c.req.param("service");
|
|
1475
|
+
if (!resolveCoverProviderPreset(service)) {
|
|
1476
|
+
return c.json({ error: "Unsupported cover service" }, 400);
|
|
1477
|
+
}
|
|
1478
|
+
const body = await c.req.json();
|
|
1479
|
+
const trimmedKey = body.apiKey?.trim() ?? "";
|
|
1480
|
+
if (trimmedKey && !isHeaderSafeApiKey(trimmedKey)) {
|
|
1481
|
+
return c.json({ error: "API Key 包含不能放入 HTTP Authorization header 的字符,请只粘贴原始密钥。" }, 400);
|
|
1482
|
+
}
|
|
1483
|
+
const secrets = await loadSecrets(root);
|
|
1484
|
+
const key = coverSecretKey(service);
|
|
1485
|
+
if (trimmedKey) {
|
|
1486
|
+
secrets.services[key] = { apiKey: trimmedKey };
|
|
1487
|
+
}
|
|
1488
|
+
else {
|
|
1489
|
+
delete secrets.services[key];
|
|
1490
|
+
}
|
|
1491
|
+
await saveSecrets(root, secrets);
|
|
1492
|
+
return c.json({ ok: true, service });
|
|
1493
|
+
});
|
|
1183
1494
|
app.delete("/api/v1/services/:service", async (c) => {
|
|
1184
1495
|
const service = c.req.param("service");
|
|
1185
1496
|
const config = await loadRawConfig(root);
|
|
@@ -1380,6 +1691,21 @@ export function createStudioServer(initialConfig, root) {
|
|
|
1380
1691
|
temperature: currentConfig.llm.temperature,
|
|
1381
1692
|
});
|
|
1382
1693
|
});
|
|
1694
|
+
app.get("/api/v1/project/files/:file{.+}", async (c) => {
|
|
1695
|
+
const file = resolveProjectImageFile(root, c.req.param("file"));
|
|
1696
|
+
try {
|
|
1697
|
+
const content = await readFile(file.resolved);
|
|
1698
|
+
return new Response(content, {
|
|
1699
|
+
headers: {
|
|
1700
|
+
"Content-Type": file.contentType,
|
|
1701
|
+
"Cache-Control": "no-store",
|
|
1702
|
+
},
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
catch {
|
|
1706
|
+
return c.notFound();
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1383
1709
|
// --- Config editing ---
|
|
1384
1710
|
app.put("/api/v1/project", async (c) => {
|
|
1385
1711
|
const updates = await c.req.json();
|
|
@@ -1638,6 +1964,40 @@ export function createStudioServer(initialConfig, root) {
|
|
|
1638
1964
|
sessionTitleBroadcasted = true;
|
|
1639
1965
|
}
|
|
1640
1966
|
};
|
|
1967
|
+
const externalEdit = await tryHandleExternalChatEdit({
|
|
1968
|
+
root,
|
|
1969
|
+
state,
|
|
1970
|
+
instruction,
|
|
1971
|
+
activeBookId: agentBookId,
|
|
1972
|
+
});
|
|
1973
|
+
if (externalEdit) {
|
|
1974
|
+
await appendManualSessionMessages(root, bookSession.sessionId, [{
|
|
1975
|
+
role: "assistant",
|
|
1976
|
+
content: [{ type: "text", text: externalEdit.responseText }],
|
|
1977
|
+
api: "anthropic-messages",
|
|
1978
|
+
provider: config.llm.provider,
|
|
1979
|
+
model: config.llm.model,
|
|
1980
|
+
usage: {
|
|
1981
|
+
input: 0,
|
|
1982
|
+
output: 0,
|
|
1983
|
+
cacheRead: 0,
|
|
1984
|
+
cacheWrite: 0,
|
|
1985
|
+
totalTokens: 0,
|
|
1986
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
1987
|
+
},
|
|
1988
|
+
stopReason: "stop",
|
|
1989
|
+
timestamp: Date.now(),
|
|
1990
|
+
}], instruction);
|
|
1991
|
+
await refreshBookSessionFromTranscript();
|
|
1992
|
+
broadcast("agent:complete", { instruction, activeBookId: externalEdit.activeBookId, sessionId: bookSession.sessionId });
|
|
1993
|
+
return c.json({
|
|
1994
|
+
response: externalEdit.responseText,
|
|
1995
|
+
session: {
|
|
1996
|
+
sessionId: bookSession.sessionId,
|
|
1997
|
+
...(externalEdit.activeBookId ? { activeBookId: externalEdit.activeBookId } : {}),
|
|
1998
|
+
},
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
1641
2001
|
// Resolve model — multi-service resolution
|
|
1642
2002
|
let resolvedModel;
|
|
1643
2003
|
let resolvedApiKey;
|
|
@@ -1758,6 +2118,7 @@ export function createStudioServer(initialConfig, root) {
|
|
|
1758
2118
|
id: toolCallId,
|
|
1759
2119
|
tool: "sub_agent",
|
|
1760
2120
|
result: toolResult,
|
|
2121
|
+
details: toolResult.details,
|
|
1761
2122
|
isError: false,
|
|
1762
2123
|
});
|
|
1763
2124
|
await appendManualSessionMessages(root, bookSession.sessionId, [{
|
|
@@ -1899,6 +2260,7 @@ export function createStudioServer(initialConfig, root) {
|
|
|
1899
2260
|
id: event.toolCallId,
|
|
1900
2261
|
tool: event.toolName,
|
|
1901
2262
|
result: event.result,
|
|
2263
|
+
details: exec?.details,
|
|
1902
2264
|
isError: event.isError,
|
|
1903
2265
|
});
|
|
1904
2266
|
}
|