dsh-log-slicer 0.1.1 → 0.1.3
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/CHANGELOG.md +13 -0
- package/package.json +7 -2
- package/src/index.js +154 -82
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
0.1.2 装进 profile 之后,重启没有任何反应——两个工具都没出现。查下来两处:
|
|
6
|
+
|
|
7
|
+
- **修**:注册的 definition 少了 `output`。宿主 `register()` 的硬要求是 `output = { schema, render }`,`render` 返回 ContentBlock[];只有 `name/description/parameters/execute` 会被直接拒。
|
|
8
|
+
- **修**:`@deepseek-ai/dsh-tools` 只是 peerDependency,装进 profile 后未必解析得到(实测 `ERR_MODULE_NOT_FOUND`)。原来解析不到就抛错,而 `apply` 又把异常吞掉了,于是整个插件静默失效。现在解析不到就自己构造一份同形状的 definition:少了参数校验,注册与调用照常。
|
|
9
|
+
|
|
10
|
+
两条一起解释了"CLI 能跑、装成插件是哑的"。
|
|
11
|
+
|
|
12
|
+
## 0.1.2
|
|
13
|
+
|
|
14
|
+
- **修**:`package.json` 里补上 `dsh.bundle.patch`,指向 `./cordis.patch.yml`。少了这一行,装成插件时宿主既不会读那个 patch,也就不会把两个工具注册进会话——CLI 照样能用,装成插件却是哑的。这是装进 profile 时才发现的问题。
|
|
15
|
+
|
|
3
16
|
## 0.1.1
|
|
4
17
|
|
|
5
18
|
三处都是第一次真用(拿 2026-10-31 上午那段去切)时暴露出来的。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-log-slicer",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Slice one section out of a DeepSeek Harness session log: verbatim turns, images, catalog row, backups and a verification hash
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Slice one section out of a DeepSeek Harness session log: verbatim turns, images, catalog row, backups and a verification hash 鈥?one command, dry-run by default.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|
|
@@ -47,6 +47,11 @@
|
|
|
47
47
|
"optional": true
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
|
+
"dsh": {
|
|
51
|
+
"bundle": {
|
|
52
|
+
"patch": "./cordis.patch.yml"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
50
55
|
"scripts": {
|
|
51
56
|
"test": "node --test"
|
|
52
57
|
}
|
package/src/index.js
CHANGED
|
@@ -5,10 +5,17 @@
|
|
|
5
5
|
* log_probe 看日志里有什么(轮次、时间、可用的锚点),只读、便宜
|
|
6
6
|
* log_slice 切一节出来;默认 dry-run,写盘要 apply === true
|
|
7
7
|
*
|
|
8
|
-
* 契约(对着 @deepseek-ai/dsh-tools
|
|
8
|
+
* 契约(对着 @deepseek-ai/dsh-tools 的 README 与 register() 的实现核对过):
|
|
9
9
|
* - export const inject = ['tools']
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
10
|
+
* - 注册的 definition 必须带 output = { schema, render },缺了会被 register() 直接拒
|
|
11
|
+
* - execute 返回的是"规范值"(JSON),render(args, value) 把它渲染成 ContentBlock[]
|
|
12
|
+
*
|
|
13
|
+
* 这里有一个实际的坑:装进 profile 之后,`@deepseek-ai/dsh-tools` 未必能从插件目录
|
|
14
|
+
* 解析到(它只是 peerDependency,宿主也没有把它放进 profile 的 node_modules)。
|
|
15
|
+
* 所以这里两手准备:能拿到 defineTool 就用它(带参数校验),拿不到就自己构造一份
|
|
16
|
+
* 同样形状的 definition —— 少了参数校验,但注册与调用都正常。
|
|
17
|
+
*
|
|
18
|
+
* apply 的异常一律吞掉:最坏是插件不工作,不能让应用起不来。
|
|
12
19
|
*/
|
|
13
20
|
|
|
14
21
|
import fs from 'node:fs';
|
|
@@ -19,22 +26,6 @@ import { indexRow, insertIndexRow, backupFile, appendSection, sha256Hex16, today
|
|
|
19
26
|
export const name = 'dsh-log-slicer';
|
|
20
27
|
export const inject = ['tools'];
|
|
21
28
|
|
|
22
|
-
async function loadDefineTool() {
|
|
23
|
-
try {
|
|
24
|
-
const mod = await import('@deepseek-ai/dsh-tools');
|
|
25
|
-
if (mod?.defineTool) return mod.defineTool;
|
|
26
|
-
} catch {
|
|
27
|
-
/* 落到环境变量 */
|
|
28
|
-
}
|
|
29
|
-
const override = process.env.DSH_TOOLS_PATH;
|
|
30
|
-
if (override) {
|
|
31
|
-
const { pathToFileURL } = await import('node:url');
|
|
32
|
-
const mod = await import(pathToFileURL(override).href);
|
|
33
|
-
if (mod?.defineTool) return mod.defineTool;
|
|
34
|
-
}
|
|
35
|
-
throw new Error('cannot resolve @deepseek-ai/dsh-tools (set DSH_TOOLS_PATH for local dev)');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
29
|
export const probeSpec = {
|
|
39
30
|
name: 'log_probe',
|
|
40
31
|
description: [
|
|
@@ -88,76 +79,157 @@ export const sliceSpec = {
|
|
|
88
79
|
},
|
|
89
80
|
};
|
|
90
81
|
|
|
91
|
-
|
|
92
|
-
|
|
82
|
+
/** 把本文件的简写参数表转成 JSON Schema(defineTool 那条路会自己转,这条路自己转)。 */
|
|
83
|
+
export function toJsonSchema(parameters = {}) {
|
|
84
|
+
const properties = {};
|
|
85
|
+
const required = [];
|
|
86
|
+
for (const [key, raw] of Object.entries(parameters)) {
|
|
87
|
+
const { required: isRequired, ...rest } = raw;
|
|
88
|
+
properties[key] = { ...rest };
|
|
89
|
+
if (isRequired) required.push(key);
|
|
90
|
+
}
|
|
91
|
+
const schema = { type: 'object', properties, additionalProperties: false };
|
|
92
|
+
if (required.length) schema.required = required;
|
|
93
|
+
return schema;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 拿 defineTool;拿不到返回 null(装进 profile 时很可能拿不到)。 */
|
|
97
|
+
async function loadDefineTool() {
|
|
98
|
+
try {
|
|
99
|
+
const mod = await import('@deepseek-ai/dsh-tools');
|
|
100
|
+
if (typeof mod?.defineTool === 'function') return mod.defineTool;
|
|
101
|
+
} catch {
|
|
102
|
+
/* 落到下面的自造 */
|
|
103
|
+
}
|
|
104
|
+
const override = process.env.DSH_TOOLS_PATH;
|
|
105
|
+
if (override) {
|
|
106
|
+
try {
|
|
107
|
+
const { pathToFileURL } = await import('node:url');
|
|
108
|
+
const mod = await import(pathToFileURL(override).href);
|
|
109
|
+
if (typeof mod?.defineTool === 'function') return mod.defineTool;
|
|
110
|
+
} catch {
|
|
111
|
+
/* 一样落到自造 */
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
93
116
|
|
|
94
|
-
|
|
95
|
-
|
|
117
|
+
/** 与 defineTool 产出同形状的 definition:register() 只要求 output.schema + output.render。 */
|
|
118
|
+
export function plainTool({ name: toolName, description, parameters, execute }) {
|
|
119
|
+
return {
|
|
120
|
+
name: toolName,
|
|
121
|
+
description,
|
|
122
|
+
parameters: toJsonSchema(parameters),
|
|
123
|
+
output: {
|
|
124
|
+
schema: { type: 'string' },
|
|
125
|
+
render: (_args, value) => [{ type: 'text', text: String(value ?? '') }],
|
|
126
|
+
},
|
|
96
127
|
async execute(args = {}) {
|
|
97
|
-
|
|
98
|
-
const width = Math.max(10, Math.min(400, Number(args.width) || 60));
|
|
99
|
-
const built = buildSection({ kind: 'chat', title: 'probe', logPath: args.logPath });
|
|
100
|
-
const turns = built.segment.length ? built.segment : [];
|
|
101
|
-
const lines = turns.slice(-limit).map((t, i) => {
|
|
102
|
-
const who = t.who === 'user' ? 'you' : 'me';
|
|
103
|
-
const when = t.ts ? new Date(Number(t.ts) || t.ts).toLocaleString() : '';
|
|
104
|
-
const body = String(t.text || '').replace(/\s+/g, ' ').slice(0, width);
|
|
105
|
-
return `${turns.length - Math.min(limit, turns.length) + i + 1}. [${who}] ${when} ${body}`;
|
|
106
|
-
});
|
|
107
|
-
return {
|
|
108
|
-
text:
|
|
109
|
-
`# ${built.log}\n` +
|
|
110
|
-
`# ${built.frames} frames (${built.failedFrames} failed) / ${built.totalTurns} turns\n\n` +
|
|
111
|
-
lines.join('\n'),
|
|
112
|
-
};
|
|
128
|
+
return execute(args);
|
|
113
129
|
},
|
|
114
|
-
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
115
132
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!args.file) throw new Error('apply=true requires "file"');
|
|
133
|
+
function define(defineTool, spec, execute) {
|
|
134
|
+
if (defineTool) {
|
|
135
|
+
return defineTool({
|
|
136
|
+
name: spec.name,
|
|
137
|
+
description: spec.description,
|
|
138
|
+
parameters: spec.parameters,
|
|
139
|
+
output: {
|
|
140
|
+
schema: { type: 'string' },
|
|
141
|
+
render: (_args, value) => [{ type: 'text', text: String(value ?? '') }],
|
|
142
|
+
},
|
|
143
|
+
async execute(args) {
|
|
144
|
+
return execute(args);
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return plainTool({ ...spec, execute });
|
|
149
|
+
}
|
|
134
150
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
151
|
+
async function probe({ logPath, limit, width } = {}) {
|
|
152
|
+
const n = Math.max(1, Math.min(500, Number(limit) || 40));
|
|
153
|
+
const w = Math.max(10, Math.min(400, Number(width) || 60));
|
|
154
|
+
const built = buildSection({ kind: 'chat', title: 'probe', logPath });
|
|
155
|
+
const turns = built.segment.length ? built.segment : [];
|
|
156
|
+
const start = turns.length - Math.min(n, turns.length);
|
|
157
|
+
const lines = turns.slice(-n).map((t, i) => {
|
|
158
|
+
const who = t.who === 'user' ? 'you' : 'me';
|
|
159
|
+
const when = t.ts ? new Date(Number(t.ts) || t.ts).toLocaleString() : '';
|
|
160
|
+
const body = String(t.text || '').replace(/\s+/g, ' ').slice(0, w);
|
|
161
|
+
return `${start + i + 1}. [${who}] ${when} ${body}`;
|
|
162
|
+
});
|
|
163
|
+
return [
|
|
164
|
+
`# ${built.log}`,
|
|
165
|
+
`# ${built.frames} frames (${built.failedFrames} failed) / ${built.totalTurns} turns`,
|
|
166
|
+
'',
|
|
167
|
+
lines.join('\n'),
|
|
168
|
+
].join('\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function slice(args = {}) {
|
|
172
|
+
const built = buildSection({
|
|
173
|
+
kind: args.kind === 'work' ? 'work' : 'chat',
|
|
174
|
+
title: args.title,
|
|
175
|
+
from: args.from,
|
|
176
|
+
to: args.to,
|
|
177
|
+
logPath: args.logPath,
|
|
158
178
|
});
|
|
179
|
+
const head = [
|
|
180
|
+
`# ${built.log}`,
|
|
181
|
+
`# ${built.frames} frames (${built.failedFrames} failed) / ${built.totalTurns} turns`,
|
|
182
|
+
`# sliced ${built.segment.length} turns (you ${built.counts.you} / me ${built.counts.me})`,
|
|
183
|
+
].join('\n');
|
|
184
|
+
|
|
185
|
+
const pointerNote = built.pointerLines?.length
|
|
186
|
+
? `\n# note: ${built.pointerLines.length} turn(s) look like pointers to a file, not the words themselves —` +
|
|
187
|
+
'\n# copy the real paragraphs from that file; if they are grading-3, do not quote them at all.' +
|
|
188
|
+
'\n# ' + built.pointerLines.map((l) => l.text).join('\n# ')
|
|
189
|
+
: '';
|
|
190
|
+
|
|
191
|
+
if (args.apply !== true) {
|
|
192
|
+
return `${head}${pointerNote}\n\n===== markdown (dry-run) =====\n\n${built.markdown}`;
|
|
193
|
+
}
|
|
194
|
+
if (!args.file) throw new Error('apply=true requires "file"');
|
|
195
|
+
|
|
196
|
+
const target = path.resolve(args.file);
|
|
197
|
+
const tag = args.tag || '自动整理之前';
|
|
198
|
+
const backup = fs.existsSync(target) ? backupFile(target, tag) : null;
|
|
199
|
+
const { after } = appendSection(target, built.markdown);
|
|
200
|
+
const volume = args.volume || path.basename(target, '.md');
|
|
201
|
+
const row = indexRow(volume, args.title, built.counts.you);
|
|
202
|
+
let indexBackup = null;
|
|
203
|
+
let indexPath = null;
|
|
204
|
+
if (args.index && fs.existsSync(args.index)) {
|
|
205
|
+
indexPath = path.resolve(args.index);
|
|
206
|
+
indexBackup = backupFile(indexPath, tag);
|
|
207
|
+
const updated = insertIndexRow(fs.readFileSync(indexPath, 'utf8'), row, volume);
|
|
208
|
+
fs.writeFileSync(indexPath, updated, 'utf8');
|
|
209
|
+
}
|
|
210
|
+
const line = `| ${today()} | ${path.basename(target)}(+${built.counts.you} 轮) | ${after} | \`${sha256Hex16(target)}\` | 本体=原件,两处一致 |`;
|
|
211
|
+
return [
|
|
212
|
+
head,
|
|
213
|
+
pointerNote,
|
|
214
|
+
'',
|
|
215
|
+
`written: ${target}`,
|
|
216
|
+
backup ? `backup: ${backup}` : '',
|
|
217
|
+
indexBackup ? `backup: ${indexBackup}` : '',
|
|
218
|
+
indexPath ? `index: ${indexPath}` : '',
|
|
219
|
+
`bytes: ${after}`,
|
|
220
|
+
`row: ${row}`,
|
|
221
|
+
`verification: ${line}`,
|
|
222
|
+
]
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
.join('\n');
|
|
225
|
+
}
|
|
159
226
|
|
|
160
|
-
|
|
227
|
+
async function registerAll(ctx) {
|
|
228
|
+
const defineTool = await loadDefineTool();
|
|
229
|
+
const disposers = [
|
|
230
|
+
ctx.tools.register(define(defineTool, probeSpec, probe)),
|
|
231
|
+
ctx.tools.register(define(defineTool, sliceSpec, slice)),
|
|
232
|
+
];
|
|
161
233
|
return () => {
|
|
162
234
|
for (const d of disposers.reverse()) d?.();
|
|
163
235
|
};
|