ship 1.4.60:Arcrun@98b45409740b

This commit is contained in:
ship
2026-08-27 22:59:34 +08:00
parent 7609f1a31b
commit 9981585df9
4 changed files with 206 additions and 67 deletions
+122 -9
View File
@@ -12978,7 +12978,87 @@ var PORTAL_TEMPLATE_SEEDS = [
// cypher-executor/src/routes/portal.ts
init_credentials();
// cypher-executor/src/lib/ai-response.ts
function pickChatContent(o) {
const choices = o.choices;
if (!Array.isArray(choices) || choices.length === 0) return void 0;
const first = choices[0];
if (!first || typeof first !== "object") return void 0;
const msg = first.message;
if (!msg || typeof msg !== "object") return void 0;
const content = msg.content;
return typeof content === "string" && content.length > 0 ? content : void 0;
}
function pickResponseValue(out) {
if (out === null || out === void 0) return void 0;
if (typeof out === "string") return out;
if (typeof out !== "object") return out;
const o = out;
const chat = pickChatContent(o);
if (chat !== void 0) return chat;
if ("response" in o) return o.response;
const result = o.result;
if (result && typeof result === "object") {
const r = result;
const rc = pickChatContent(r);
if (rc !== void 0) return rc;
if ("response" in r) return r.response;
}
return void 0;
}
function normalizeAiText(out) {
const v = pickResponseValue(out);
if (v === null || v === void 0) return { text: "", kind: "empty" };
if (typeof v === "string") return { text: v.trim(), kind: v.trim() ? "string" : "empty" };
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") {
return { text: String(v), kind: "scalar" };
}
if (typeof v === "object") {
try {
const s = JSON.stringify(v);
if (typeof s !== "string") {
return { text: "", kind: "unrenderable", reason: "JSON.stringify \u56DE undefined" };
}
return { text: s, kind: "json" };
} catch (e) {
return {
text: "",
kind: "unrenderable",
reason: e instanceof Error ? e.message : "\u7121\u6CD5\u5E8F\u5217\u5316"
};
}
}
return { text: "", kind: "unrenderable", reason: `\u672A\u9810\u671F\u7684\u578B\u5225 ${typeof v}` };
}
function parseExpectShape(raw2, hasPrompt) {
const s = String(raw2 ?? "").trim();
if (s === "json_object" || s === "text") return s;
return hasPrompt ? "json_object" : "text";
}
function hasJsonObject(text) {
const start = text.indexOf("{");
const end = text.lastIndexOf("}");
if (start < 0 || end <= start) return { ok: false, reason: "\u6574\u6BB5\u56DE\u61C9\u88E1\u627E\u4E0D\u5230 JSON \u7269\u4EF6\uFF08\u6C92\u6709\u6210\u5C0D\u7684\u5927\u62EC\u865F\uFF09" };
try {
const parsed = JSON.parse(text.slice(start, end + 1));
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return { ok: false, reason: "\u5927\u62EC\u865F\u88E1\u7684\u5167\u5BB9\u4E0D\u662F JSON \u7269\u4EF6" };
}
return { ok: true };
} catch (e) {
return { ok: false, reason: `JSON \u89E3\u6790\u5931\u6557\uFF1A${e instanceof Error ? e.message : "\u672A\u77E5\u932F\u8AA4"}` };
}
}
function snippetForError(text, max = 120) {
const one = text.replace(/\s+/g, " ").trim();
if (!one) return "\uFF08\u7A7A\u7684\uFF09";
return one.length <= max ? one : one.slice(0, max) + "\u2026";
}
// cypher-executor/src/routes/portal.ts
var portalRouter = new Hono2();
var EXTRACT_MODEL = "@cf/meta/llama-4-scout-17b-16e-instruct";
var SESSION_PREFIX2 = "portal_sess:";
var LOCKFAIL_PREFIX = "portal_lockfail:";
var LOCK_LIMIT = 5;
@@ -13983,6 +14063,7 @@ portalRouter.post(
const pageName = String(body?.page_name ?? "").trim();
const srcText = String(body?.text ?? "");
const daemonPrompt = String(body?.prompt ?? "").trim();
const expectShape = parseExpectShape(body?.expect, Boolean(daemonPrompt));
if (!daemonPrompt && (!pageName || !srcText.trim()))
return c.json({ error: "page_name \u8207 text \u5FC5\u586B" }, 400);
if (!c.env.AI) {
@@ -14003,15 +14084,44 @@ portalRouter.post(
\u539F\u7A3F\uFF1A
${srcText}`;
try {
const out = await c.env.AI.run("@cf/meta/llama-4-scout-17b-16e-instruct", {
const out = await c.env.AI.run(EXTRACT_MODEL, {
messages: [{ role: "user", content: prompt }],
max_tokens: daemonPrompt ? 8192 : 2048,
temperature: 0.2
temperature: 0.2,
// 🔴 Arcrun#1342026-08-27):呼叫端說它要一個 JSON 物件 ⇒ **就用模型保證得了的方式去要**。
// 不加這個的實測(youlin 真檔《火星座標_短片劇本_v1》打 6 次):**3 次壞在
// 模型把字串的收尾引號打成全形 `”`** ⇒ 字串沒收掉 ⇒ 後面那個換行變成 JSON 裡的
// 裸控制字元 ⇒ daemon 與雲端都解析不了。那是第二顆骰子,和 `[object Object]`
// 不同源但同一種傷害:**同一份檔、同一個請求,成敗看運氣**。
// `response_format: json_object` 走的是受限解碼,語法由平台保證 ⇒ 骰子拿掉。
// 只在呼叫端宣告要 JSON 時才加:legacy(要 markdown 卡)那條路一個字都沒動。
...expectShape === "json_object" ? { response_format: { type: "json_object" } } : {}
// 🔴 **不准把回傳值宣告成 `{ response?: string }`**。
// 那個型別斷言正是本票的病根——binding 把純 JSON 回應解析成 JS 物件交回來,
// 而斷言讓編譯器相信它是字串 ⇒ `String(物件)` '[object Object]' ⇒ 整張卡沒了。
// 一律 `unknown`,由 normalizeAiText 認形狀(見 lib/ai-response.ts 檔頭實測)。
});
const raw2 = String(out?.response ?? "").trim();
const norm = normalizeAiText(out);
if (norm.kind === "unrenderable") {
return c.json({ error: `\u96F2\u7AEF\u8403\u53D6\uFF1AWorkers AI \u7684\u56DE\u61C9\u9084\u539F\u4E0D\u56DE\u6587\u5B57\uFF08${norm.reason ?? "\u672A\u77E5\u539F\u56E0"}\uFF09` }, 502);
}
const raw2 = norm.text;
if (!raw2) return c.json({ error: "Workers AI \u6C92\u6709\u56DE\u50B3\u5167\u5BB9" }, 502);
if (daemonPrompt) {
return c.json({ success: true, output: raw2 });
if (expectShape === "json_object") {
const check = hasJsonObject(raw2);
if (!check.ok) {
return c.json(
{
error: `\u96F2\u7AEF\u8403\u53D6\uFF1A\u6A21\u578B\u6C92\u6709\u7167\u5951\u7D04\u56DE JSON \u7269\u4EF6\uFF08${check.reason}\uFF09\u3002\u9019\u4E00\u6BB5\u662F\u5728\u4F60\u7684\u77E5\u8B58\u5EAB\u96F2\u7AEF\u8DD1\u7684\uFF08Workers AI ${EXTRACT_MODEL}\uFF09\uFF0C\u4E0D\u662F\u5728\u4F60\u7684\u96FB\u8166\u4E0A\u3002\u6A21\u578B\u5BE6\u969B\u56DE\u7684\u524D 120 \u5B57\uFF1A${snippetForError(raw2)}`,
code: "ai_output_not_json_object",
output_kind: norm.kind
},
502
);
}
}
return c.json({ success: true, output: raw2, output_kind: norm.kind });
}
const marker = `# ${pageName}`;
const idx = raw2.lastIndexOf(marker);
@@ -17099,7 +17209,7 @@ async function tripletCensus(env, tenant2) {
if (owned !== 0) return { owned, any: null };
return { owned, any: await tripletCount(env, null) };
}
async function fuzzyFindNode(env, tenant2, searchTerm) {
async function fuzzyFindNode(env, tenant2, searchTerm, libraries) {
try {
const res = await kbdbFetch(env, `/records/by-template/triplet?${ownerQuery(tenant2)}`);
if (!res.ok) return null;
@@ -17109,6 +17219,8 @@ async function fuzzyFindNode(env, tenant2, searchTerm) {
for (const r of body.records) {
const v = r?.values;
if (!v || typeof v !== "object") continue;
const lib = recordLibrary(v);
if (lib !== null && !canReadLibrary(libraries, lib)) continue;
if (typeof v.subject === "string" && v.subject.trim()) nodeNames.add(v.subject.trim());
if (typeof v.object === "string" && v.object.trim()) nodeNames.add(v.object.trim());
}
@@ -17170,10 +17282,11 @@ portalDataRouter.get(
return c.json({ success: true, entry });
})
);
async function fetchNeighborsFromKbdb(env, tenant2, node, depth) {
async function fetchNeighborsFromKbdb(env, tenant2, node, depth, libraries) {
const qs = new URLSearchParams();
qs.set("depth", String(depth));
qs.set("template", "triplet");
if (!libraries.includes("*")) qs.set("library", libraries.join(","));
const res = await kbdbFetch(env, `/graph/neighbors/${encodeURIComponent(node)}?${qs.toString()}&${ownerQuery(tenant2)}`);
const body = await res.json().catch(() => null);
return { ok: res.ok, status: res.status, body };
@@ -17197,16 +17310,16 @@ portalDataRouter.get(
let first = null;
try {
for (const name of tryNames) {
const r = await fetchNeighborsFromKbdb(c.env, tenant2, name, depth);
const r = await fetchNeighborsFromKbdb(c.env, tenant2, name, depth, libraries);
if (!first) first = r;
if (!r.ok) break;
const mapped = mapGraphNeighborsResponse(r.body);
if (mapped.count > 0) return c.json(mapped);
}
if (first?.ok) {
const fallbackName = await fuzzyFindNode(c.env, tenant2, rawName);
const fallbackName = await fuzzyFindNode(c.env, tenant2, rawName, libraries);
if (fallbackName && !tryNames.includes(fallbackName)) {
const r = await fetchNeighborsFromKbdb(c.env, tenant2, fallbackName, depth);
const r = await fetchNeighborsFromKbdb(c.env, tenant2, fallbackName, depth, libraries);
if (r.ok) {
const mapped = mapGraphNeighborsResponse(r.body);
if (mapped.count > 0) return c.json(mapped);