@jayyuen66/dsh-plugin-shared 0.1.0 → 0.1.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.
@@ -1,43 +1 @@
1
- import { defineConfig } from "vitest/config";
2
- //#region config/vitest.base.ts
3
- /**
4
- * 公共面:13 份配置里逐字相同的那部分,从今往后才真的只有一处。
5
- * `testTimeout: 20_000` 是 12 个包的共同值(vitest 默认 5000),shared 原先没写、
6
- * 现在继承这个值只会放宽不会收紧,故不会把任何已过用例变成不稳定。
7
- */
8
- function definePackageConfig(options) {
9
- const { coverageInclude, test } = options;
10
- return defineConfig({ test: {
11
- include: ["test/**/*.test.ts"],
12
- environment: "node",
13
- testTimeout: 2e4,
14
- coverage: {
15
- provider: "v8",
16
- include: coverageInclude,
17
- exclude: [
18
- "test/**",
19
- "node_modules/**",
20
- "build-client*",
21
- "client.js",
22
- "agents/**",
23
- "scripts/**",
24
- "**/*.d.mts"
25
- ],
26
- reporter: [
27
- "text",
28
- "json-summary",
29
- "json"
30
- ],
31
- reportsDirectory: "coverage",
32
- thresholds: {
33
- lines: 100,
34
- statements: 100,
35
- functions: 100,
36
- branches: 100
37
- }
38
- },
39
- ...test
40
- } });
41
- }
42
- //#endregion
43
- export { definePackageConfig };
1
+ import{defineConfig as e}from"vitest/config";function t(t){let{coverageInclude:n,test:r}=t;return e({test:{include:[`test/**/*.test.ts`],environment:`node`,testTimeout:2e4,coverage:{provider:`v8`,include:n,exclude:[`test/**`,`node_modules/**`,`build-client*`,`client.js`,`agents/**`,`scripts/**`,`**/*.d.mts`],reporter:[`text`,`json-summary`,`json`],reportsDirectory:`coverage`,thresholds:{lines:100,statements:100,functions:100,branches:100}},...r}})}export{t as definePackageConfig};
package/dist/errors.js CHANGED
@@ -1,7 +1 @@
1
- //#region lib/errors.ts
2
- /** `Error` 取 `message`,其余一律 `String(error)`(`null` → `"null"`,对象 → 其 `toString()`)。 */
3
- function errorText(error) {
4
- return error instanceof Error ? error.message : String(error);
5
- }
6
- //#endregion
7
- export { errorText };
1
+ function e(e){let t=Object.getOwnPropertyDescriptor(e,`message`);return t!==void 0&&`value`in t&&typeof t.value==`string`?t.value:void 0}function t(n,r){if(r.has(n))return`<circular cause>`;r.add(n);try{if(!(n instanceof Error)){if(typeof n==`object`&&n){let t=e(n);if(t!==void 0)return t}return String(n)}let i=n.message===``?n.name:n.message,a=n instanceof AggregateError&&n.errors.length>0?` [${n.errors.map(e=>t(e,r)).join(`; `)}]`:``,o=n.cause===void 0||n.cause===null?``:t(n.cause,r);return`${i}${a}${o===``||o===i?``:`: ${o}`}`}catch{return`<unrenderable value>`}finally{r.delete(n)}}function n(e){return t(e,/* @__PURE__ */ new Set)}export{n as errorText};
package/dist/http.js CHANGED
@@ -1,121 +1 @@
1
- //#region lib/http.ts
2
- /** 写 JSON 响应(no-store:端点都是运行时状态,禁缓存防陈旧)。
3
- * 已发头/已结束则静默跳过:同一响应被写两次会抛 ERR_HTTP_HEADERS_SENT,
4
- * 而端点多在 async 回调里,抛错变成 unhandled rejection 拖垮共享的 web host。 */
5
- function sendJson(res, status, payload) {
6
- if (res.headersSent || res.writableEnded) return;
7
- res.writeHead(status, {
8
- "content-type": "application/json; charset=utf-8",
9
- "cache-control": "no-store"
10
- });
11
- res.end(JSON.stringify(payload));
12
- }
13
- /** 读单值请求头:重复头取首项,非字符串/无 headers 面(部分 mock)归空串。 */
14
- function headerValue(req, name) {
15
- const value = req.headers?.[name];
16
- const first = Array.isArray(value) ? value[0] : value;
17
- return typeof first === "string" ? first : "";
18
- }
19
- /**
20
- * CORS 同源纵深防御:浏览器请求带 sec-fetch-site,非 same-origin/none 即拒绝。
21
- * 空头(curl/CLI)不拦——浏览器侧伪造不了同源判定,CLI 是本地可信调用面。
22
- */
23
- function isCrossOrigin(req) {
24
- const site = headerValue(req, "sec-fetch-site");
25
- return site !== "" && site !== "same-origin" && site !== "none";
26
- }
27
- /** 从 req.url 读 query 参数(URLSearchParams 语义:+ 解码空格、尊重 ; 分隔等)。 */
28
- function queryParam(req, name) {
29
- try {
30
- return new URL(req.url ?? "", "http://localhost").searchParams.get(name);
31
- } catch {
32
- return null;
33
- }
34
- }
35
- /** CSRF 校验:header 值非空且等于下发 token(sec-fetch-site 只防浏览器,可伪造)。 */
36
- function checkCsrf(req, token, headerName) {
37
- const headerVal = headerValue(req, headerName);
38
- return headerVal !== "" && headerVal === token;
39
- }
40
- /** 请求块 → Buffer(流通常给 Buffer,被 `setEncoding` 改成 string 时按 UTF-8 编码回来)。 */
41
- function chunkToBuffer(chunk) {
42
- return Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
43
- }
44
- /**
45
- * 读 POST body(**按 UTF-8 字节**计上限;先查 content-length 再收流)。
46
- *
47
- * 两个必须一次做对的点:
48
- * 1. 逐块 `String(chunk)` 会把跨 chunk 边界的多字节字符(中文/emoji)解码成
49
- * U+FFFD 且仍是合法 JSON —— 静默写坏用户数据。故累积 Buffer 后一次性解码。
50
- * 2. 只按解码后字符数设限,恶意 body 可在触顶前已占满内存;content-length
51
- * 预检让明显超限的请求根本不开始累积。
52
- * 超限/坏流都立即停止读取(for-await 提前 return 会 release 迭代器并销毁流)。
53
- */
54
- async function readBody(req, maxBytes) {
55
- const declared = Number(headerValue(req, "content-length"));
56
- if (Number.isFinite(declared) && declared > maxBytes) return {
57
- ok: false,
58
- reason: "too-large"
59
- };
60
- const chunks = [];
61
- let bytes = 0;
62
- try {
63
- for await (const raw of req) {
64
- const chunk = chunkToBuffer(raw);
65
- bytes += chunk.length;
66
- if (bytes > maxBytes) return {
67
- ok: false,
68
- reason: "too-large"
69
- };
70
- chunks.push(chunk);
71
- }
72
- } catch {
73
- return {
74
- ok: false,
75
- reason: "aborted"
76
- };
77
- }
78
- return {
79
- ok: true,
80
- text: Buffer.concat(chunks).toString("utf8")
81
- };
82
- }
83
- /**
84
- * 一次性校验组合(通用 POST 端点样板):跨域拒绝 → CSRF 拒绝 → 读 body →
85
- * 413(超限)/400(流中断)。全过返回 body 字符串。缺 CSRF 规范(如 GET 或
86
- * 自持 token 端点)时传 skipCsrf=true。
87
- */
88
- async function guardBody(req, res, opts) {
89
- if (isCrossOrigin(req)) {
90
- sendJson(res, 403, {
91
- ok: false,
92
- error: "cross-origin request rejected"
93
- });
94
- return null;
95
- }
96
- if (opts.csrf !== void 0 && !checkCsrf(req, opts.csrf.token, opts.csrf.headerName)) {
97
- sendJson(res, 403, {
98
- ok: false,
99
- error: "invalid csrf token"
100
- });
101
- return null;
102
- }
103
- const read = await readBody(req, opts.maxBytes);
104
- if (!read.ok) {
105
- if (read.reason === "too-large") {
106
- sendJson(res, 413, {
107
- ok: false,
108
- error: "request body too large"
109
- });
110
- return null;
111
- }
112
- sendJson(res, 400, {
113
- ok: false,
114
- error: "request body unreadable"
115
- });
116
- return null;
117
- }
118
- return read.text;
119
- }
120
- //#endregion
121
- export { checkCsrf, guardBody, isCrossOrigin, queryParam, readBody, sendJson };
1
+ function e(e,t,n){if(e.headersSent||e.writableEnded)return;let r=JSON.stringify(n);e.writeHead(t,{"content-type":`application/json; charset=utf-8`,"cache-control":`no-store`}),e.end(r)}function t(e,t){let n=e.headers?.[t],r=Array.isArray(n)?n[0]:n;return typeof r==`string`?r:``}function n(e){let n=t(e,`sec-fetch-site`);return n!==``&&n!==`same-origin`&&n!==`none`}function r(e,t){try{return new URL(e.url??``,`http://localhost`).searchParams.get(t)}catch{return null}}function i(e,n,r){let i=t(e,r);return i!==``&&i===n}const a={"too-large":{status:413,error:`request body too large`},aborted:{status:400,error:`request body unreadable`},"bad-budget":{status:500,error:`request body limit misconfigured`}};function o(e){return Buffer.isBuffer(e)?e:Buffer.from(String(e),`utf8`)}async function s(e,n){if(!Number.isFinite(n)||n<0)return{ok:!1,reason:`bad-budget`};let r=Number(t(e,`content-length`));if(Number.isFinite(r)&&r>n)return{ok:!1,reason:`too-large`};let i=[],a=0;try{for await(let t of e){let e=o(t);if(a+=e.length,a>n)return{ok:!1,reason:`too-large`};i.push(e)}}catch{return{ok:!1,reason:`aborted`}}return{ok:!0,text:Buffer.concat(i).toString(`utf8`)}}const c=`cross-origin request rejected`;async function l(t,r,o){if(n(t))return e(r,403,{ok:!1,error:c}),null;if(o.csrf!==void 0&&!i(t,o.csrf.token,o.csrf.headerName))return e(r,403,{ok:!1,error:`invalid csrf token`}),null;let l=await s(t,o.maxBytes);if(l.ok)return l.text;let u=a[l.reason];return e(r,u.status,{ok:!1,error:u.error}),null}export{c as CROSS_ORIGIN_TEXT,i as checkCsrf,l as guardBody,n as isCrossOrigin,r as queryParam,s as readBody,e as sendJson};
package/dist/index.js CHANGED
@@ -1,13 +1 @@
1
- import { checkCsrf, guardBody, isCrossOrigin, queryParam, readBody, sendJson } from "./http.js";
2
- import { fieldOf, isRecord } from "./record.js";
3
- import { editPathOf, parseToolArguments, resultIsError, scanToolEvents, toolArgumentsBad, toolEventRowsOf } from "./tool-events.js";
4
- import { claimApply } from "./card-apply.js";
5
- import { deriveProjectKey } from "./project-key.js";
6
- import { DEFAULT_LOCALE, LOCALE_PREFERENCE_FIELD, LOCALE_SETTINGS_NAMESPACE, messagesFor, resolveLocale, resolveLocalePreference } from "./locale.js";
7
- import { settleLessonCall } from "./lesson-bus.js";
8
- import { truncateEnd, truncateStart } from "./text.js";
9
- import { errorText } from "./errors.js";
10
- import { shrinkJsonlTail } from "./jsonl.js";
11
- import { guardTrust, requestTrust, trustRejectionText } from "./trust.js";
12
- import { jobOutcomeOf } from "./job-outcome.js";
13
- export { DEFAULT_LOCALE, LOCALE_PREFERENCE_FIELD, LOCALE_SETTINGS_NAMESPACE, checkCsrf, claimApply, deriveProjectKey, editPathOf, errorText, fieldOf, guardBody, guardTrust, isCrossOrigin, isRecord, jobOutcomeOf, messagesFor, parseToolArguments, queryParam, readBody, requestTrust, resolveLocale, resolveLocalePreference, resultIsError, scanToolEvents, sendJson, settleLessonCall, shrinkJsonlTail, toolArgumentsBad, toolEventRowsOf, truncateEnd, truncateStart, trustRejectionText };
1
+ import{CROSS_ORIGIN_TEXT as e,checkCsrf as t,guardBody as n,isCrossOrigin as r,queryParam as i,readBody as a,sendJson as o}from"./http.js";import{fieldOf as s,isRecord as c}from"./record.js";import{editPathOf as l,parseToolArguments as u,scanToolEvents as d,toolArgumentsBad as f,toolEventRowsOf as p}from"./tool-events.js";import{claimApply as m}from"./card-apply.js";import{deriveProjectKey as h}from"./project-key.js";import{DEFAULT_LOCALE as g,LOCALE_PREFERENCE_FIELD as _,LOCALE_SETTINGS_NAMESPACE as v,messagesFor as y,resolveLocale as b,resolveLocalePreference as x}from"./locale.js";import{settleLessonCall as S}from"./lesson-bus.js";import{truncateEnd as C,truncateStart as w}from"./text.js";import{errorText as T}from"./errors.js";import{shrinkJsonlTail as E}from"./jsonl.js";import{guardTrust as D,requestTrust as O,trustRejectionText as k}from"./trust.js";import{jobOutcomeOf as A}from"./job-outcome.js";export{e as CROSS_ORIGIN_TEXT,g as DEFAULT_LOCALE,_ as LOCALE_PREFERENCE_FIELD,v as LOCALE_SETTINGS_NAMESPACE,t as checkCsrf,m as claimApply,h as deriveProjectKey,l as editPathOf,T as errorText,s as fieldOf,n as guardBody,D as guardTrust,r as isCrossOrigin,c as isRecord,A as jobOutcomeOf,y as messagesFor,u as parseToolArguments,i as queryParam,a as readBody,O as requestTrust,b as resolveLocale,x as resolveLocalePreference,d as scanToolEvents,o as sendJson,S as settleLessonCall,E as shrinkJsonlTail,f as toolArgumentsBad,p as toolEventRowsOf,C as truncateEnd,w as truncateStart,k as trustRejectionText};
@@ -1,18 +1 @@
1
- //#region lib/job-outcome.ts
2
- /**
3
- * 落定进程 → 作业结局。
4
- * @param proc 已落定的子进程句柄(`done` 已 resolve 之后再读,值才是终态)
5
- * @returns 交给 {@link JobRegistry} 生产者的结局:killed / completed,理由在 detail
6
- */
7
- function jobOutcomeOf(proc) {
8
- if (proc.status === "killed") return {
9
- status: "killed",
10
- detail: proc.signal === null ? "killed before exit" : `signal: ${proc.signal}`
11
- };
12
- return {
13
- status: "completed",
14
- detail: `exit code: ${String(proc.exitCode ?? 0)}`
15
- };
16
- }
17
- //#endregion
18
- export { jobOutcomeOf };
1
+ function e(e){return e.status===`killed`?{status:`killed`,detail:e.signal===null?`killed before exit`:`signal: ${e.signal}`}:{status:`completed`,detail:`exit code: ${String(e.exitCode??0)}`}}export{e as jobOutcomeOf};
package/dist/jsonl.js CHANGED
@@ -1,20 +1,4 @@
1
- //#region lib/jsonl.ts
2
- /**
3
- * 反复保留后半行段直到 UTF-8 字节数 ≤ `maxBytes`;行数 ≤2 时停手(单条超限行原样保留,
4
- * 防死循环也防清空文件)。未超限直接返回入参**同一引用**,调用方可据此跳过写盘。
5
- * @param text 整份 JSONL 文本
6
- * @param maxBytes 字节上限
7
- * @returns 收缩后的文本
8
- */
9
- function shrinkJsonlTail(text, maxBytes) {
10
- let current = text;
11
- while (Buffer.byteLength(current, "utf8") > maxBytes) {
12
- const lines = current.split("\n");
13
- if (lines.length <= 2) break;
14
- const keep = lines.slice(Math.floor(lines.length / 2)).join("\n");
15
- current = keep.startsWith("\n") ? keep.slice(1) : keep;
16
- }
17
- return current;
18
- }
19
- //#endregion
20
- export { shrinkJsonlTail };
1
+ function e(e,t){let n=e;for(;Buffer.byteLength(n,`utf8`)>t;){let e=n.split(`
2
+ `);if(e.length<=2)break;let t=e.slice(Math.floor(e.length/2)).join(`
3
+ `);n=t.startsWith(`
4
+ `)?t.slice(1):t}return n}export{e as shrinkJsonlTail};
@@ -1,43 +1 @@
1
- //#region lib/lesson-bus.ts
2
- /**
3
- * 值是否为可等待对象。逐层收窄而不是 instanceof Promise:宿主给的 Promise 可能来自另一份
4
- * realm,认 then 才不会把「没接住 rejection」误判成「已处理」。刻意不认函数型 thenable——
5
- * 本仓唯一的 provider 是设置服务,它给的是真 Promise,不额外养一条走不到的分支。
6
- * @param value - 待判定的调用返回值。
7
- * @returns true 表示要给它挂一个 rejection 出口。
8
- */
9
- function isThenable(value) {
10
- if (typeof value !== "object" || value === null) return false;
11
- return typeof Reflect.get(value, "then") === "function";
12
- }
13
- /**
14
- * 等一次可等待调用落定,只把拒绝送进失败收口。
15
- *
16
- * 用 `await` 而不是 `.then(null, onFailure)`(本仓的 async/await 侧口径,见 lint 基线
17
- * `promise/prefer-await-to-then`):await 自身就是一个 rejection 出口,挂上即视为已处理;
18
- * fulfil 侧无需动作(回执不参与调用方判定)。
19
- * @param thenable - 调用返回的可等待值。
20
- * @param onFailure - 失败收口,与同步抛错共用。
21
- */
22
- async function drainRejection(thenable, onFailure) {
23
- try {
24
- await thenable;
25
- } catch (error) {
26
- onFailure(error);
27
- }
28
- }
29
- /**
30
- * 执行一次 lesson bus 调用,并把两类失败归到同一个出口。
31
- * @param call - 真正的调用(同步返回或返回 Promise 都接受)。
32
- * @param onFailure - 失败收口:同步抛错与异步拒绝都会到这里,且各自只被调一次。
33
- */
34
- function settleLessonCall(call, onFailure) {
35
- try {
36
- const settled = call();
37
- if (isThenable(settled)) drainRejection(settled, onFailure);
38
- } catch (error) {
39
- onFailure(error);
40
- }
41
- }
42
- //#endregion
43
- export { settleLessonCall };
1
+ function e(e){return e===null||typeof e!=`object`&&typeof e!=`function`?!1:typeof Reflect.get(e,`then`)==`function`}function t(e,t){try{t(e)}catch(e){console.error(`[shared/lesson-bus] onFailure threw:`,e)}}async function n(e,n){try{await e}catch(e){t(e,n)}}function r(r,i){try{let t=r();e(t)&&n(t,i)}catch(e){t(e,i)}}export{r as settleLessonCall};
package/dist/locale.js CHANGED
@@ -1,43 +1 @@
1
- import { isRecord } from "./record.js";
2
- //#region lib/locale.ts
3
- /** 缺省语言。设置项未设或值不合语法时落到这里。 */
4
- const DEFAULT_LOCALE = "zh";
5
- /**
6
- * 官方 `@deepseek-ai/dsh-client-locale` 在 Host settings 文档里拥有的命名空间与字段名
7
- * (其 `lib/index.js:5-7`)。这里是**抄常量不是引依赖**:那是宿主内部实现,没进公开
8
- * 依赖面,而两侧必须同名才能读到同一份偏好。
9
- */
10
- const LOCALE_SETTINGS_NAMESPACE = "locale";
11
- const LOCALE_PREFERENCE_FIELD = "preference";
12
- /**
13
- * 把设置里的原始值归一成受支持的 locale:按**主语言子标签**判定(`en-US`、`zh-Hans-CN`
14
- * 都落位),与 client 半「先完整标签、再主语言子标签」的规则同源;判不出来一律退
15
- * DEFAULT_LOCALE——文案语言不该成为报错面。
16
- * @param raw - 设置里的原始值,类型不可信(来自用户文档)。
17
- * @returns 归一后的 locale。
18
- */
19
- function resolveLocale(raw) {
20
- const [primary] = (typeof raw === "string" ? raw.trim().toLowerCase() : "").split("-");
21
- return primary === "en" || primary === "zh" ? primary : "zh";
22
- }
23
- /**
24
- * 从 `ctx.settings.describe(LOCALE_SETTINGS_NAMESPACE)` 的返回值里取偏好并归一。
25
- * describe 出来的形状由别的包拥有,所以入参按 unknown 处理:非对象、数组、缺字段
26
- * 都走默认语言,不抛。
27
- * @param described - describe 的原始返回值(可能 undefined)。
28
- * @returns 归一后的 locale。
29
- */
30
- function resolveLocalePreference(described) {
31
- return resolveLocale(isRecord(described) ? described[LOCALE_PREFERENCE_FIELD] : void 0);
32
- }
33
- /**
34
- * 取某个语言下的消息表。
35
- * @param catalog - 两语齐全的消息表。
36
- * @param locale - 已归一的语言。
37
- * @returns 对应语言的消息表(引用返回,不做拷贝)。
38
- */
39
- function messagesFor(catalog, locale) {
40
- return locale === "en" ? catalog.en : catalog.zh;
41
- }
42
- //#endregion
43
- export { DEFAULT_LOCALE, LOCALE_PREFERENCE_FIELD, LOCALE_SETTINGS_NAMESPACE, messagesFor, resolveLocale, resolveLocalePreference };
1
+ import{isRecord as e}from"./record.js";const t=`zh`,n=`locale`,r=`preference`;function i(e){let[t]=(typeof e==`string`?e.trim().toLowerCase():``).split(`-`);return t===`en`||t===`zh`?t:`zh`}function a(t){return i(e(t)?t[r]:void 0)}function o(e,t){return e[t]}export{t as DEFAULT_LOCALE,r as LOCALE_PREFERENCE_FIELD,n as LOCALE_SETTINGS_NAMESPACE,o as messagesFor,i as resolveLocale,a as resolveLocalePreference};
@@ -1,23 +1 @@
1
- import { realpathSync } from "node:fs";
2
- import { createHash } from "node:crypto";
3
- import path from "node:path";
4
- //#region lib/project-key.ts
5
- /**
6
- * cwd → 项目桶键。非字符串 / trim 后为空 / 归一后落到根目录 → `"default"`
7
- * (与记忆网关的兜底桶名一致)。
8
- */
9
- function deriveProjectKey(cwd, opts) {
10
- if (typeof cwd !== "string") return "default";
11
- const trimmed = cwd.trim();
12
- if (trimmed === "") return "default";
13
- const realpath = opts?.realpath ?? realpathSync;
14
- let resolved = path.resolve(trimmed);
15
- try {
16
- resolved = realpath(resolved);
17
- } catch {}
18
- const norm = resolved.replaceAll(/\/{2,}/gu, "/").replace(/\/+$/u, "");
19
- if (norm === "") return "default";
20
- return `${norm.slice(norm.lastIndexOf("/") + 1).replaceAll(/\s+/gu, "_")}-${createHash("sha256").update(norm, "utf8").digest("hex").slice(0, 8)}`;
21
- }
22
- //#endregion
23
- export { deriveProjectKey };
1
+ import{realpathSync as e}from"node:fs";import{createHash as t}from"node:crypto";import n from"node:path";const r=/[/\\]+/gu,i=/\/{2,}/gu;function a(e){return e===`\\`?r:i}const o=/^[A-Za-z]:$/u,s=/^\\\\[^\\]+\\[^\\]+$/u;function c(r,i){if(typeof r!=`string`)return`default`;let c=r.trim();if(c===``)return`default`;let l=i?.realpath??e,u=i?.sep??n.sep,d=(u===`\\`?n.win32:n.posix).resolve(c);try{d=l(d)}catch{}let f=d.replace(/[/\\]+$/u,``),p=d.replaceAll(a(u),`/`).replace(/\/+$/u,``);return p===``||o.test(p)||s.test(f)?`default`:`${p.slice(p.lastIndexOf(`/`)+1).replaceAll(/\s+/gu,`_`)}-${t(`sha256`).update(p,`utf8`).digest(`hex`).slice(0,8)}`}export{c as deriveProjectKey};
package/dist/record.js CHANGED
@@ -1,11 +1 @@
1
- //#region lib/record.ts
2
- /** 对象类型守卫:非 `null` 的 `object` 且非数组(`Record` 索引签名投影的基础)。 */
3
- function isRecord(value) {
4
- return typeof value === "object" && value !== null && !Array.isArray(value);
5
- }
6
- /** 从 `unknown` 投影单个字段;非 `Record` → `undefined`。 */
7
- function fieldOf(value, key) {
8
- return isRecord(value) ? value[key] : void 0;
9
- }
10
- //#endregion
11
- export { fieldOf, isRecord };
1
+ function e(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function t(t,n){return e(t)?t[n]:void 0}export{t as fieldOf,e as isRecord};
package/dist/text.js CHANGED
@@ -1,24 +1 @@
1
- import { truncateWithoutSplittingSurrogatePair } from "@deepseek-ai/dsh-output-retention";
2
- //#region lib/text.ts
3
- const LOW_SURROGATE_START = 56320;
4
- const LOW_SURROGATE_END = 57343;
5
- /**
6
- * 保留前 `maxChars` 个码元;切点切开代理对时丢掉尾部那个孤立高代理。
7
- * `maxChars <= 0` 与 NaN ⇒ 空串。
8
- */
9
- function truncateEnd(text, maxChars) {
10
- return truncateWithoutSplittingSurrogatePair(text, Math.max(0, maxChars));
11
- }
12
- /**
13
- * 保留后 `minChars` 个码元;切点切开代理对时丢掉首部那个孤立低代理。
14
- * `minChars <= 0` 与 NaN 与 `truncateEnd(text, 0)` 同口径返回空串,不返回整串。
15
- */
16
- function truncateStart(text, minChars) {
17
- if (minChars <= 0 || Number.isNaN(minChars)) return "";
18
- if (text.length <= minChars) return text;
19
- const cut = text.slice(-minChars);
20
- const first = cut.codePointAt(0);
21
- return first !== void 0 && first >= LOW_SURROGATE_START && first <= LOW_SURROGATE_END ? cut.slice(1) : cut;
22
- }
23
- //#endregion
24
- export { truncateEnd, truncateStart };
1
+ import{truncateWithoutSplittingSurrogatePair as e}from"@deepseek-ai/dsh-output-retention";function t(t,n){return e(t,Math.max(0,n))}function n(e,t){if(t<=0||Number.isNaN(t))return``;if(e.length<=t)return e;let n=e.slice(-t),r=n.codePointAt(0);return r!==void 0&&r>=56320&&r<=57343?n.slice(1):n}export{t as truncateEnd,n as truncateStart};
@@ -1,165 +1 @@
1
- import { isRecord } from "./record.js";
2
- //#region lib/tool-events.ts
3
- /** 从 message 面读 v4 的成败位;非对象/缺位一律按成功。 */
4
- function readIsError(raw) {
5
- return isRecord(raw) && raw["isError"] === true;
6
- }
7
- /** 显式 true 才算真(缺失/非布尔一律 false):官方把 PTC 结算的 isError 标成必选
8
- * boolean,但类型挡不住重放数据里缺位的情形,缺位必须按成功而不是 undefined。 */
9
- function isTrueFlag(value) {
10
- return value === true;
11
- }
12
- /** 官方保证每个事件必带对象 `data`。这里仍校验:扫描器跑在门禁/证据链的同步热路径上,
13
- * 契约是「零星坏事件静默跳过、不抛错」,而重放或跨边界数据可以送来临时缺 data 的事件。
14
- * 类型面已把 data 记为必选,所以这个门槛在 TS 里是恒真——它防的是类型外的输入。 */
15
- function hasObjectData(event) {
16
- return isRecord(event.data);
17
- }
18
- /**
19
- * 官方把 `callId` / `subCallId` 声明为 `ToolCallId`(品牌串,必选)。这里仍校验字符串
20
- * 形态并按 `string` 交出:类型面挡不住跨持久化/进程边界送来的坏契约,而「把非串当真实
21
- * 串联键」会让 tool/result 与 tool/call 错配。空串同样归 undefined——空 callId 与缺席
22
- * 一样不可信,不得冒充键(记账面因此是 `string | undefined` 而非官方品牌类型)。
23
- */
24
- function usableCallId(value) {
25
- return typeof value === "string" && value.length > 0 ? value : void 0;
26
- }
27
- /**
28
- * tool/result 的 isError 判定(官方 v4:成败位在 message 级、类型为 `?: boolean`;
29
- * 字段缺失按成功)。非 tool/result 事件一律按成功——PTC 子调度的成败位在 data 顶层,
30
- * 由 `resultRecordOf` 的对应分支处理,不从这儿走。
31
- */
32
- function resultIsError(event) {
33
- return event.type === "tool/result" && readIsError(event.data.message);
34
- }
35
- /** 解析 tool/call 的 arguments:string JSON 文本或已解析对象;非法/缺失 → 空对象。 */
36
- function parseToolArguments(raw) {
37
- if (typeof raw === "string") try {
38
- const parsed = JSON.parse(raw);
39
- return isRecord(parsed) ? parsed : {};
40
- } catch {
41
- return {};
42
- }
43
- return isRecord(raw) ? raw : {};
44
- }
45
- /**
46
- * arguments 是否「无法参与记账」:仅当字符串 JSON **语法非法**(JSON.parse 抛错)。
47
- * 合法 JSON 非对象("42")、undefined/数字/对象等一律不标——quality-gate 原实现
48
- * 对 JSON.parse 抛错判 EDIT_SKIP,但对"解析成功却非对象"仍记账(path=undefined),
49
- * 本标记必须精确复用那一个语义点,不能把"非对象"也划进坏参。
50
- */
51
- function toolArgumentsBad(raw) {
52
- if (typeof raw === "string") try {
53
- JSON.parse(raw);
54
- return false;
55
- } catch {
56
- return true;
57
- }
58
- return false;
59
- }
60
- /** 调用类事件(tool/call 与 PTC 子调用开始)→ 记账记录;其余返回 undefined。 */
61
- function callRecordOf(event, index) {
62
- let record;
63
- if (!hasObjectData(event)) return record;
64
- if (event.type === "tool/call") record = {
65
- name: event.data.name,
66
- callId: usableCallId(event.data.callId),
67
- arguments: parseToolArguments(event.data.arguments),
68
- badArguments: toolArgumentsBad(event.data.arguments),
69
- seq: index
70
- };
71
- else if (event.type === "tool/ptc-dispatch-start") {
72
- const subCallId = usableCallId(event.data.subCallId);
73
- if (subCallId !== void 0) record = {
74
- name: event.data.name,
75
- callId: subCallId,
76
- arguments: parseToolArguments(event.data.arguments),
77
- badArguments: toolArgumentsBad(event.data.arguments),
78
- seq: index
79
- };
80
- }
81
- return record;
82
- }
83
- /** 结算类事件(tool/result 与 PTC 子调度结算)→ 成败记录;其余返回 undefined。 */
84
- function resultRecordOf(event, index) {
85
- let record;
86
- if (!hasObjectData(event)) return record;
87
- if (event.type === "tool/result") {
88
- const message = event.data.message;
89
- const source = isRecord(message) ? message["source"] : void 0;
90
- record = {
91
- callId: isRecord(source) ? usableCallId(source["callId"]) : void 0,
92
- isError: readIsError(message),
93
- seq: index
94
- };
95
- } else if (event.type === "tool/ptc-dispatch") {
96
- const subCallId = usableCallId(event.data.subCallId);
97
- if (subCallId !== void 0) record = {
98
- callId: subCallId,
99
- isError: isTrueFlag(event.data.isError),
100
- seq: index
101
- };
102
- }
103
- return record;
104
- }
105
- /**
106
- * **单条事件**的记账行(`scanToolEvents` 的一步,也是它唯一的实现处)。
107
- *
108
- * 之所以导出:danger-guard 把同一套台账折成 `sessionProjections` 投影单元(增量折叠)后,
109
- * 折叠与"注册表缺席时的全量扫描"必须逐字同源——两处各写一遍字段读取,就会在两行上给出
110
- * 不同的证据链(那是安全门禁的 fail-open)。折叠侧传 `event.seq`,扫描侧传数组下标,
111
- * 两者在官方 `seq = log.length` 的连号契约下同一个值。
112
- */
113
- function toolEventRowsOf(event, index) {
114
- return {
115
- call: callRecordOf(event, index),
116
- result: resultRecordOf(event, index)
117
- };
118
- }
119
- /**
120
- * 扫描事件流 → tool/call 与 tool/result 两张表。
121
- * 只收集形状可用的事件,其余静默跳过——调用方(门禁/证据链)以空表安全降级,不因零星
122
- * 坏事件抛错。数组空洞(类型面不可表示、重放数据里却可能出现)同样跳过。
123
- * seq 取事件在入参数组里的下标(与 danger-guard scanToolCalls 的归因语义一致)。
124
- */
125
- function scanToolEvents(events) {
126
- const calls = [];
127
- const results = [];
128
- for (let index = 0; index < events.length; index += 1) {
129
- const event = events[index];
130
- if (event !== void 0 && isRecord(event)) {
131
- const { call, result } = toolEventRowsOf(event, index);
132
- if (call !== void 0) calls.push(call);
133
- if (result !== void 0) results.push(result);
134
- }
135
- }
136
- return {
137
- calls,
138
- results
139
- };
140
- }
141
- function pathOf(args, key) {
142
- const value = args[key];
143
- return typeof value === "string" ? value : void 0;
144
- }
145
- /**
146
- * 由 tool/call 记录推导被编辑的文件路径;str_replace_editor 的 view 只读不算写。
147
- * @returns { kind: "write", path } 写操作;{ kind: "read-view", path } 只读视图;
148
- * { kind: "skip" } 无路径或参数形状不可用(调用方忽略)。
149
- */
150
- function editPathOf(call) {
151
- const args = call.arguments;
152
- if (call.name === "str_replace_editor") return args["command"] === "view" ? {
153
- kind: "read-view",
154
- path: pathOf(args, "path")
155
- } : {
156
- kind: "write",
157
- path: pathOf(args, "path")
158
- };
159
- return {
160
- kind: "write",
161
- path: pathOf(args, "file_path")
162
- };
163
- }
164
- //#endregion
165
- export { editPathOf, parseToolArguments, resultIsError, scanToolEvents, toolArgumentsBad, toolEventRowsOf };
1
+ import{isRecord as e}from"./record.js";function t(t){return e(t)&&t.isError===!0}function n(e){return e===!0}function r(t){return e(t.data)}function i(e){return typeof e==`string`&&e.length>0?e:void 0}function a(t){if(typeof t==`string`)try{let n=JSON.parse(t);return e(n)?n:{}}catch{return{}}return e(t)?t:{}}function o(e){if(typeof e==`string`)try{return JSON.parse(e),!1}catch{return!0}return!1}function s(e,t){let n;if(!r(e))return n;if(e.type===`tool/call`)n={name:e.data.name,callId:i(e.data.callId),arguments:a(e.data.arguments),badArguments:o(e.data.arguments),seq:t};else if(e.type===`tool/ptc-dispatch-start`){let r=i(e.data.subCallId);r!==void 0&&(n={name:e.data.name,callId:r,arguments:a(e.data.arguments),badArguments:o(e.data.arguments),seq:t})}return n}function c(a,o){let s;if(!r(a))return s;if(a.type===`tool/result`){let n=a.data.message,r=e(n)?n.source:void 0;s={callId:e(r)?i(r.callId):void 0,isError:t(n),seq:o}}else if(a.type===`tool/ptc-dispatch`){let e=i(a.data.subCallId);e!==void 0&&(s={callId:e,isError:n(a.data.isError),seq:o})}return s}function l(e,t){return{call:s(e,t),result:c(e,t)}}function u(t){let n=[],r=[];for(let[i,a]of t.entries())if(e(a)){let{call:e,result:t}=l(a,i);e!==void 0&&n.push(e),t!==void 0&&r.push(t)}return{calls:n,results:r}}function d(e,t){let n=e[t];return typeof n==`string`?n:void 0}function f(e){let t=e.arguments;return e.name===`str_replace_editor`?t.command===`view`?{kind:`read-view`,path:d(t,`path`)}:{kind:`write`,path:d(t,`path`)}:{kind:`write`,path:d(t,`file_path`)}}export{f as editPathOf,a as parseToolArguments,u as scanToolEvents,o as toolArgumentsBad,l as toolEventRowsOf};