@aiwayds/dsh-topics-memory 0.11.0 → 0.12.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/lib/commands.js +68 -39
- package/package.json +1 -1
- package/src/commands.ts +69 -38
package/lib/commands.js
CHANGED
|
@@ -86,18 +86,17 @@ function fail(text) {
|
|
|
86
86
|
async function renderStatus(service) {
|
|
87
87
|
const s = await service.store.status();
|
|
88
88
|
const cfg = service.cfg;
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
` 蒸馏:${cfg.distillProvider !== '' && cfg.distillModel !== '' ? `${cfg.distillProvider}/${cfg.distillModel},每 ${cfg.distillEveryTurns} 轮` : '未配置模型(/topics set distill-provider / distill-model)'}`,
|
|
89
|
+
const rows = [
|
|
90
|
+
['模式', service.githubMode ? `github(${cell(cfg.repo)})` : 'local-only'],
|
|
91
|
+
['Topics', `${s.topicCount}(draft ${s.byStatus.draft} / stable ${s.byStatus.stable} / deprecated ${s.byStatus.deprecated})`],
|
|
92
|
+
['观察积压', `${s.observationsPending} 未蒸馏 / ${s.observationsTotal} 总量`],
|
|
93
|
+
['冲突', s.conflicts.length === 0 ? '无' : s.conflicts.map((c) => cell(c)).join('、')],
|
|
94
|
+
['损坏文件', s.broken.length === 0 ? '无' : s.broken.map((b) => cell(b)).join('、')],
|
|
95
|
+
['git', s.git ? `是(HEAD ${s.head?.slice(0, 10) ?? '??'})` : '否'],
|
|
96
|
+
['注入', cfg.autoInject ? `${cfg.injectMode === 'digest' ? 'digest' : 'pointer'} 模式(topK ${cfg.topK},预算 ${cfg.injectMode === 'digest' ? cfg.totalBudget : Math.min(cfg.totalBudget, 600)} tok,阈值 ${cfg.matchThreshold})` : '关'],
|
|
97
|
+
['注入去重', cfg.injectDedup ? '开(同会话已注入的 Topic 不重注)' : '关'],
|
|
98
|
+
['慢道', cfg.qualityLane === 'off' ? '关' : cfg.qualityLane === 'always' ? `每轮(${cfg.distillProvider}/${cfg.distillModel})` : `采样 1/3(${cfg.distillProvider}/${cfg.distillModel})`],
|
|
99
|
+
['蒸馏', cfg.distillProvider !== '' && cfg.distillModel !== '' ? `${cfg.distillProvider}/${cfg.distillModel},每 ${cfg.distillEveryTurns} 轮` : '未配置模型(/topics set distill-provider / distill-model)'],
|
|
101
100
|
];
|
|
102
101
|
// Last lane outcome (distill-state summary) — what "checkable via
|
|
103
102
|
// /topics status" promises; absent until the first run of this bundle.
|
|
@@ -107,14 +106,20 @@ async function renderStatus(service) {
|
|
|
107
106
|
? `标记 ${String(lastRun.marked ?? 0)} 条观察`
|
|
108
107
|
: `失败(${String(lastRun.reason ?? 'unknown')})`;
|
|
109
108
|
const gc = typeof lastRun.gcDropped === 'number' && lastRun.gcDropped > 0 ? `,GC 回收 ${lastRun.gcDropped}` : '';
|
|
110
|
-
|
|
109
|
+
rows.push(['最近蒸馏', `${outcome}${gc} @ ${cell(String(lastRun.at ?? '').slice(0, 19).replace('T', ' '))}`]);
|
|
111
110
|
}
|
|
112
111
|
if (service.sync !== undefined) {
|
|
113
|
-
|
|
112
|
+
rows.push(['上次推送', cell(String(service.sync.lastPushAt ?? '从未'))]);
|
|
114
113
|
if (service.sync.lastError !== '')
|
|
115
|
-
|
|
114
|
+
rows.push(['同步错误', cell(service.sync.lastError.split('\n').at(-1) ?? '')]);
|
|
116
115
|
}
|
|
117
|
-
return
|
|
116
|
+
return [
|
|
117
|
+
`dsh-topics-memory @ ${s.root}`,
|
|
118
|
+
'',
|
|
119
|
+
'| 字段 | 值 |',
|
|
120
|
+
'| --- | --- |',
|
|
121
|
+
...rows.map(([k, v]) => `| ${k} | ${v} |`),
|
|
122
|
+
].join('\n');
|
|
118
123
|
}
|
|
119
124
|
/**
|
|
120
125
|
* `/topics distill` — one manual distill run over the current observation pool.
|
|
@@ -151,9 +156,13 @@ async function renderStats(service) {
|
|
|
151
156
|
return '还没有注入记录 —— 用起来之后这里会有 hit rate / top-N / near-miss 分布。';
|
|
152
157
|
const lines = [
|
|
153
158
|
`注入统计(最近 ${records.length} 轮):`,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
'',
|
|
160
|
+
'| 指标 | 值 |',
|
|
161
|
+
'| --- | --- |',
|
|
162
|
+
`| hit rate | ${(stats.hitRate * 100).toFixed(1)}%(${stats.injectedRounds}/${stats.rounds} 轮注入) |`,
|
|
163
|
+
`| 零命中轮 | ${stats.zeroHitRounds} |`,
|
|
164
|
+
`| 平均命中 | ${stats.avgHitsPerRound} 条/轮 |`,
|
|
165
|
+
`| 平均预算占用 | ${stats.avgBudgetUtilization} tok |`,
|
|
157
166
|
];
|
|
158
167
|
// v4 lane split — how much of the injection traffic each lane carries.
|
|
159
168
|
const slowRounds = records.filter((r) => r.lane === 'slow' || r.lane === 'mixed').length;
|
|
@@ -166,7 +175,7 @@ async function renderStats(service) {
|
|
|
166
175
|
.sort((a, b) => a - b);
|
|
167
176
|
medianLag = `,赶上中位时延 ${(lags[Math.floor(lags.length / 2)] / 1000).toFixed(1)}s`;
|
|
168
177
|
}
|
|
169
|
-
lines.push(
|
|
178
|
+
lines.push(`| 慢道参与轮 | ${slowRounds}(快 ${records.length - slowRounds} / 慢或混合 ${slowRounds}${medianLag}) |`);
|
|
170
179
|
}
|
|
171
180
|
// Pointer open rate (v4 §4.3): topic_open calls vs pointer entries injected.
|
|
172
181
|
// Echoed hits ride the retrieval but never became pointers — excluded here.
|
|
@@ -176,24 +185,24 @@ async function renderStats(service) {
|
|
|
176
185
|
r.hits.filter((h) => !(r.deduped ?? []).includes(h.slug) && !(r.echoed ?? []).includes(h.slug)).length +
|
|
177
186
|
(r.slow?.length ?? 0), 0);
|
|
178
187
|
const rate = entries === 0 ? 0 : Math.min(1, opens.length / entries);
|
|
179
|
-
lines.push(
|
|
188
|
+
lines.push(`| 指针打开率 | ${(rate * 100).toFixed(1)}%(${opens.length} 次 topic_open / ${entries} 条注入指针) |`);
|
|
180
189
|
}
|
|
181
190
|
const echoedCount = records.reduce((acc, r) => acc + (r.echoed?.length ?? 0), 0);
|
|
182
191
|
if (echoedCount > 0) {
|
|
183
|
-
lines.push(
|
|
192
|
+
lines.push(`| 回声抑制 | ${echoedCount} 次(本会话蒸馏出的 topic 不回注) |`);
|
|
184
193
|
}
|
|
185
194
|
if (stats.topTopics.length > 0) {
|
|
186
|
-
lines.push('
|
|
195
|
+
lines.push('', 'Top-N 被注入 Topic:', '', '| Slug | 注入次数 |', '| --- | --- |');
|
|
187
196
|
for (const t of stats.topTopics.slice(0, 5))
|
|
188
|
-
lines.push(
|
|
197
|
+
lines.push(`| \`${cell(t.slug)}\` | ${t.count} |`);
|
|
189
198
|
}
|
|
190
199
|
if (stats.nearMissHistogram.length > 0) {
|
|
191
|
-
lines.push('
|
|
200
|
+
lines.push('', 'Near-miss 分布(低于阈值或被结构门挡下):', '', '| 分数段 | 次数 |', '| --- | --- |');
|
|
192
201
|
for (const b of stats.nearMissHistogram)
|
|
193
|
-
lines.push(
|
|
202
|
+
lines.push(`| ${cell(b.bucket)} | ${b.count} |`);
|
|
194
203
|
const hint = tuningHint(stats, service.cfg.matchThreshold);
|
|
195
204
|
if (hint !== undefined)
|
|
196
|
-
lines.push(
|
|
205
|
+
lines.push('', `💡 ${hint}`);
|
|
197
206
|
}
|
|
198
207
|
return lines.join('\n');
|
|
199
208
|
}
|
|
@@ -207,20 +216,31 @@ export function tuningHint(stats, threshold) {
|
|
|
207
216
|
}
|
|
208
217
|
return undefined;
|
|
209
218
|
}
|
|
219
|
+
/** /topics list caps the table at this many newest topics. */
|
|
220
|
+
const LIST_LIMIT = 100;
|
|
210
221
|
async function renderList(service) {
|
|
211
222
|
const metas = await service.store.listTopics();
|
|
212
223
|
if (metas.length === 0)
|
|
213
224
|
return 'Bundle 里还没有 Topic —— 在会话里让我记点什么,或 /topics set 配置好蒸馏。';
|
|
214
|
-
|
|
225
|
+
// Newest first, capped: with a large bundle the table is for scanning
|
|
226
|
+
// recent work, not an exhaustive roster — /topics show <slug> reaches any
|
|
227
|
+
// topic the cap hides. (store.ts re-stamps generated.at on every topic
|
|
228
|
+
// write, so it IS the updated-at time; the slug tiebreak keeps same-stamp
|
|
229
|
+
// rows stable.)
|
|
230
|
+
metas.sort((a, b) => b.generatedAt.localeCompare(a.generatedAt) || a.slug.localeCompare(b.slug));
|
|
231
|
+
const shown = metas.slice(0, LIST_LIMIT);
|
|
215
232
|
const lines = [
|
|
216
|
-
`共 ${metas.length} 个 Topic
|
|
233
|
+
`共 ${metas.length} 个 Topic(按最近更新排序):`,
|
|
217
234
|
'',
|
|
218
|
-
'| Slug | 标题 | 状态 | 标签 | 更新 |',
|
|
219
|
-
'| --- | --- | --- | --- | --- |',
|
|
235
|
+
'| # | Slug | 标题 | 状态 | 标签 | 更新 |',
|
|
236
|
+
'| --- | --- | --- | --- | --- | --- |',
|
|
220
237
|
];
|
|
221
|
-
|
|
238
|
+
shown.forEach((m, i) => {
|
|
222
239
|
const tags = m.tags.length > 0 ? m.tags.map((t) => `#${cell(t)}`).join(' ') : '—';
|
|
223
|
-
lines.push(`| \`${cell(m.slug)}\` | ${cell(m.title)} | ${m.status} | ${tags} | ${cell(m.generatedAt.slice(0, 10))} |`);
|
|
240
|
+
lines.push(`| ${i + 1} | \`${cell(m.slug)}\` | ${cell(m.title)} | ${m.status} | ${tags} | ${cell(m.generatedAt.slice(0, 10))} |`);
|
|
241
|
+
});
|
|
242
|
+
if (metas.length > shown.length) {
|
|
243
|
+
lines.push('', `… 其余 ${metas.length - shown.length} 个更早的 Topic 未显示(/topics show <slug> 直达)。`);
|
|
224
244
|
}
|
|
225
245
|
return lines.join('\n');
|
|
226
246
|
}
|
|
@@ -254,11 +274,15 @@ async function renderHistory(service, slug) {
|
|
|
254
274
|
const { entries } = await service.history(slug, 30);
|
|
255
275
|
if (entries.length === 0)
|
|
256
276
|
return fail(`Topic “${slug}” 没有历史(不存在或 bundle 不是 git 仓库)`);
|
|
257
|
-
const lines = [
|
|
277
|
+
const lines = [
|
|
278
|
+
`${slug} 的变更史(${entries.length} 条):`,
|
|
279
|
+
'',
|
|
280
|
+
'| Hash | 时间 | 变更 | 当时的结论 |',
|
|
281
|
+
'| --- | --- | --- | --- |',
|
|
282
|
+
];
|
|
258
283
|
for (const e of entries) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
lines.push(` └ 结论当时:${e.conclusion}`);
|
|
284
|
+
const conclusion = e.conclusion !== undefined && e.conclusion !== '' ? cell(e.conclusion) : '—';
|
|
285
|
+
lines.push(`| \`${cell(e.hash)}\` | ${cell(e.date.slice(0, 19).replace('T', ' '))} | ${cell(e.message)} | ${conclusion} |`);
|
|
262
286
|
}
|
|
263
287
|
return ok(lines.join('\n'));
|
|
264
288
|
}
|
|
@@ -316,9 +340,14 @@ async function doSync(service, direction) {
|
|
|
316
340
|
return fail('用法:/topics sync [pull|push]');
|
|
317
341
|
}
|
|
318
342
|
function renderConfig(cfg) {
|
|
319
|
-
const lines = [
|
|
343
|
+
const lines = [
|
|
344
|
+
'当前配置:',
|
|
345
|
+
'',
|
|
346
|
+
'| 配置项 | 值 |',
|
|
347
|
+
'| --- | --- |',
|
|
348
|
+
];
|
|
320
349
|
for (const key of CONFIG_KEYS) {
|
|
321
|
-
lines.push(
|
|
350
|
+
lines.push(`| ${displayKey(key)} | ${cell(String(cfg[key]))} |`);
|
|
322
351
|
}
|
|
323
352
|
return lines.join('\n');
|
|
324
353
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwayds/dsh-topics-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "dsh-plugin: quality-first topic memory for dsh — a topic keeps the starting question, conclusion, impact and dependencies, not the process; local-first git-traced OKF bundle, budgeted LLM-free injection",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/src/commands.ts
CHANGED
|
@@ -114,18 +114,17 @@ function fail(text: string): CommandResult {
|
|
|
114
114
|
async function renderStatus(service: TopicsService): Promise<string> {
|
|
115
115
|
const s = await service.store.status()
|
|
116
116
|
const cfg = service.cfg
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
` 蒸馏:${cfg.distillProvider !== '' && cfg.distillModel !== '' ? `${cfg.distillProvider}/${cfg.distillModel},每 ${cfg.distillEveryTurns} 轮` : '未配置模型(/topics set distill-provider / distill-model)'}`,
|
|
117
|
+
const rows: [string, string][] = [
|
|
118
|
+
['模式', service.githubMode ? `github(${cell(cfg.repo)})` : 'local-only'],
|
|
119
|
+
['Topics', `${s.topicCount}(draft ${s.byStatus.draft} / stable ${s.byStatus.stable} / deprecated ${s.byStatus.deprecated})`],
|
|
120
|
+
['观察积压', `${s.observationsPending} 未蒸馏 / ${s.observationsTotal} 总量`],
|
|
121
|
+
['冲突', s.conflicts.length === 0 ? '无' : s.conflicts.map((c) => cell(c)).join('、')],
|
|
122
|
+
['损坏文件', s.broken.length === 0 ? '无' : s.broken.map((b) => cell(b)).join('、')],
|
|
123
|
+
['git', s.git ? `是(HEAD ${s.head?.slice(0, 10) ?? '??'})` : '否'],
|
|
124
|
+
['注入', cfg.autoInject ? `${cfg.injectMode === 'digest' ? 'digest' : 'pointer'} 模式(topK ${cfg.topK},预算 ${cfg.injectMode === 'digest' ? cfg.totalBudget : Math.min(cfg.totalBudget, 600)} tok,阈值 ${cfg.matchThreshold})` : '关'],
|
|
125
|
+
['注入去重', cfg.injectDedup ? '开(同会话已注入的 Topic 不重注)' : '关'],
|
|
126
|
+
['慢道', cfg.qualityLane === 'off' ? '关' : cfg.qualityLane === 'always' ? `每轮(${cfg.distillProvider}/${cfg.distillModel})` : `采样 1/3(${cfg.distillProvider}/${cfg.distillModel})`],
|
|
127
|
+
['蒸馏', cfg.distillProvider !== '' && cfg.distillModel !== '' ? `${cfg.distillProvider}/${cfg.distillModel},每 ${cfg.distillEveryTurns} 轮` : '未配置模型(/topics set distill-provider / distill-model)'],
|
|
129
128
|
]
|
|
130
129
|
// Last lane outcome (distill-state summary) — what "checkable via
|
|
131
130
|
// /topics status" promises; absent until the first run of this bundle.
|
|
@@ -136,13 +135,19 @@ async function renderStatus(service: TopicsService): Promise<string> {
|
|
|
136
135
|
? `标记 ${String(lastRun.marked ?? 0)} 条观察`
|
|
137
136
|
: `失败(${String(lastRun.reason ?? 'unknown')})`
|
|
138
137
|
const gc = typeof lastRun.gcDropped === 'number' && lastRun.gcDropped > 0 ? `,GC 回收 ${lastRun.gcDropped}` : ''
|
|
139
|
-
|
|
138
|
+
rows.push(['最近蒸馏', `${outcome}${gc} @ ${cell(String(lastRun.at ?? '').slice(0, 19).replace('T', ' '))}`])
|
|
140
139
|
}
|
|
141
140
|
if (service.sync !== undefined) {
|
|
142
|
-
|
|
143
|
-
if (service.sync.lastError !== '')
|
|
141
|
+
rows.push(['上次推送', cell(String(service.sync.lastPushAt ?? '从未'))])
|
|
142
|
+
if (service.sync.lastError !== '') rows.push(['同步错误', cell(service.sync.lastError.split('\n').at(-1) ?? '')])
|
|
144
143
|
}
|
|
145
|
-
return
|
|
144
|
+
return [
|
|
145
|
+
`dsh-topics-memory @ ${s.root}`,
|
|
146
|
+
'',
|
|
147
|
+
'| 字段 | 值 |',
|
|
148
|
+
'| --- | --- |',
|
|
149
|
+
...rows.map(([k, v]) => `| ${k} | ${v} |`),
|
|
150
|
+
].join('\n')
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
/**
|
|
@@ -178,9 +183,13 @@ async function renderStats(service: TopicsService): Promise<string> {
|
|
|
178
183
|
if (records.length === 0) return '还没有注入记录 —— 用起来之后这里会有 hit rate / top-N / near-miss 分布。'
|
|
179
184
|
const lines = [
|
|
180
185
|
`注入统计(最近 ${records.length} 轮):`,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
186
|
+
'',
|
|
187
|
+
'| 指标 | 值 |',
|
|
188
|
+
'| --- | --- |',
|
|
189
|
+
`| hit rate | ${(stats.hitRate * 100).toFixed(1)}%(${stats.injectedRounds}/${stats.rounds} 轮注入) |`,
|
|
190
|
+
`| 零命中轮 | ${stats.zeroHitRounds} |`,
|
|
191
|
+
`| 平均命中 | ${stats.avgHitsPerRound} 条/轮 |`,
|
|
192
|
+
`| 平均预算占用 | ${stats.avgBudgetUtilization} tok |`,
|
|
184
193
|
]
|
|
185
194
|
// v4 lane split — how much of the injection traffic each lane carries.
|
|
186
195
|
const slowRounds = records.filter((r) => r.lane === 'slow' || r.lane === 'mixed').length
|
|
@@ -193,7 +202,7 @@ async function renderStats(service: TopicsService): Promise<string> {
|
|
|
193
202
|
.sort((a, b) => a - b)
|
|
194
203
|
medianLag = `,赶上中位时延 ${(lags[Math.floor(lags.length / 2)] / 1000).toFixed(1)}s`
|
|
195
204
|
}
|
|
196
|
-
lines.push(
|
|
205
|
+
lines.push(`| 慢道参与轮 | ${slowRounds}(快 ${records.length - slowRounds} / 慢或混合 ${slowRounds}${medianLag}) |`)
|
|
197
206
|
}
|
|
198
207
|
// Pointer open rate (v4 §4.3): topic_open calls vs pointer entries injected.
|
|
199
208
|
// Echoed hits ride the retrieval but never became pointers — excluded here.
|
|
@@ -207,21 +216,21 @@ async function renderStats(service: TopicsService): Promise<string> {
|
|
|
207
216
|
0,
|
|
208
217
|
)
|
|
209
218
|
const rate = entries === 0 ? 0 : Math.min(1, opens.length / entries)
|
|
210
|
-
lines.push(
|
|
219
|
+
lines.push(`| 指针打开率 | ${(rate * 100).toFixed(1)}%(${opens.length} 次 topic_open / ${entries} 条注入指针) |`)
|
|
211
220
|
}
|
|
212
221
|
const echoedCount = records.reduce((acc, r) => acc + (r.echoed?.length ?? 0), 0)
|
|
213
222
|
if (echoedCount > 0) {
|
|
214
|
-
lines.push(
|
|
223
|
+
lines.push(`| 回声抑制 | ${echoedCount} 次(本会话蒸馏出的 topic 不回注) |`)
|
|
215
224
|
}
|
|
216
225
|
if (stats.topTopics.length > 0) {
|
|
217
|
-
lines.push('
|
|
218
|
-
for (const t of stats.topTopics.slice(0, 5)) lines.push(
|
|
226
|
+
lines.push('', 'Top-N 被注入 Topic:', '', '| Slug | 注入次数 |', '| --- | --- |')
|
|
227
|
+
for (const t of stats.topTopics.slice(0, 5)) lines.push(`| \`${cell(t.slug)}\` | ${t.count} |`)
|
|
219
228
|
}
|
|
220
229
|
if (stats.nearMissHistogram.length > 0) {
|
|
221
|
-
lines.push('
|
|
222
|
-
for (const b of stats.nearMissHistogram) lines.push(
|
|
230
|
+
lines.push('', 'Near-miss 分布(低于阈值或被结构门挡下):', '', '| 分数段 | 次数 |', '| --- | --- |')
|
|
231
|
+
for (const b of stats.nearMissHistogram) lines.push(`| ${cell(b.bucket)} | ${b.count} |`)
|
|
223
232
|
const hint = tuningHint(stats, service.cfg.matchThreshold)
|
|
224
|
-
if (hint !== undefined) lines.push(
|
|
233
|
+
if (hint !== undefined) lines.push('', `💡 ${hint}`)
|
|
225
234
|
}
|
|
226
235
|
return lines.join('\n')
|
|
227
236
|
}
|
|
@@ -237,19 +246,31 @@ export function tuningHint(stats: ReturnType<typeof aggregateStats>, threshold:
|
|
|
237
246
|
return undefined
|
|
238
247
|
}
|
|
239
248
|
|
|
249
|
+
/** /topics list caps the table at this many newest topics. */
|
|
250
|
+
const LIST_LIMIT = 100
|
|
251
|
+
|
|
240
252
|
async function renderList(service: TopicsService): Promise<string> {
|
|
241
253
|
const metas = await service.store.listTopics()
|
|
242
254
|
if (metas.length === 0) return 'Bundle 里还没有 Topic —— 在会话里让我记点什么,或 /topics set 配置好蒸馏。'
|
|
243
|
-
|
|
255
|
+
// Newest first, capped: with a large bundle the table is for scanning
|
|
256
|
+
// recent work, not an exhaustive roster — /topics show <slug> reaches any
|
|
257
|
+
// topic the cap hides. (store.ts re-stamps generated.at on every topic
|
|
258
|
+
// write, so it IS the updated-at time; the slug tiebreak keeps same-stamp
|
|
259
|
+
// rows stable.)
|
|
260
|
+
metas.sort((a, b) => b.generatedAt.localeCompare(a.generatedAt) || a.slug.localeCompare(b.slug))
|
|
261
|
+
const shown = metas.slice(0, LIST_LIMIT)
|
|
244
262
|
const lines = [
|
|
245
|
-
`共 ${metas.length} 个 Topic
|
|
263
|
+
`共 ${metas.length} 个 Topic(按最近更新排序):`,
|
|
246
264
|
'',
|
|
247
|
-
'| Slug | 标题 | 状态 | 标签 | 更新 |',
|
|
248
|
-
'| --- | --- | --- | --- | --- |',
|
|
265
|
+
'| # | Slug | 标题 | 状态 | 标签 | 更新 |',
|
|
266
|
+
'| --- | --- | --- | --- | --- | --- |',
|
|
249
267
|
]
|
|
250
|
-
|
|
268
|
+
shown.forEach((m, i) => {
|
|
251
269
|
const tags = m.tags.length > 0 ? m.tags.map((t) => `#${cell(t)}`).join(' ') : '—'
|
|
252
|
-
lines.push(`| \`${cell(m.slug)}\` | ${cell(m.title)} | ${m.status} | ${tags} | ${cell(m.generatedAt.slice(0, 10))} |`)
|
|
270
|
+
lines.push(`| ${i + 1} | \`${cell(m.slug)}\` | ${cell(m.title)} | ${m.status} | ${tags} | ${cell(m.generatedAt.slice(0, 10))} |`)
|
|
271
|
+
})
|
|
272
|
+
if (metas.length > shown.length) {
|
|
273
|
+
lines.push('', `… 其余 ${metas.length - shown.length} 个更早的 Topic 未显示(/topics show <slug> 直达)。`)
|
|
253
274
|
}
|
|
254
275
|
return lines.join('\n')
|
|
255
276
|
}
|
|
@@ -281,10 +302,15 @@ async function renderHistory(service: TopicsService, slug: string | undefined):
|
|
|
281
302
|
if (slug === undefined || slug === '') return fail('用法:/topics history <slug>')
|
|
282
303
|
const { entries } = await service.history(slug, 30)
|
|
283
304
|
if (entries.length === 0) return fail(`Topic “${slug}” 没有历史(不存在或 bundle 不是 git 仓库)`)
|
|
284
|
-
const lines = [
|
|
305
|
+
const lines = [
|
|
306
|
+
`${slug} 的变更史(${entries.length} 条):`,
|
|
307
|
+
'',
|
|
308
|
+
'| Hash | 时间 | 变更 | 当时的结论 |',
|
|
309
|
+
'| --- | --- | --- | --- |',
|
|
310
|
+
]
|
|
285
311
|
for (const e of entries) {
|
|
286
|
-
|
|
287
|
-
|
|
312
|
+
const conclusion = e.conclusion !== undefined && e.conclusion !== '' ? cell(e.conclusion) : '—'
|
|
313
|
+
lines.push(`| \`${cell(e.hash)}\` | ${cell(e.date.slice(0, 19).replace('T', ' '))} | ${cell(e.message)} | ${conclusion} |`)
|
|
288
314
|
}
|
|
289
315
|
return ok(lines.join('\n'))
|
|
290
316
|
}
|
|
@@ -341,9 +367,14 @@ async function doSync(service: TopicsService, direction: string | undefined): Pr
|
|
|
341
367
|
}
|
|
342
368
|
|
|
343
369
|
function renderConfig(cfg: TopicsConfigValue): string {
|
|
344
|
-
const lines = [
|
|
370
|
+
const lines = [
|
|
371
|
+
'当前配置:',
|
|
372
|
+
'',
|
|
373
|
+
'| 配置项 | 值 |',
|
|
374
|
+
'| --- | --- |',
|
|
375
|
+
]
|
|
345
376
|
for (const key of CONFIG_KEYS) {
|
|
346
|
-
lines.push(
|
|
377
|
+
lines.push(`| ${displayKey(key)} | ${cell(String(cfg[key]))} |`)
|
|
347
378
|
}
|
|
348
379
|
return lines.join('\n')
|
|
349
380
|
}
|