@huaqiu/dsh-tool-kicad-schematic-gen 0.6.4 → 0.6.5
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/client.js +158 -30
- package/lib/index.d.ts +1 -1
- package/lib/index.js +95 -2
- package/package.json +5 -4
- package/src/client/index.ts +30 -14
- package/src/client/schematic-ref.ts +183 -28
- package/src/index.ts +11 -2
- package/src/prompt.ts +131 -0
package/lib/client.js
CHANGED
|
@@ -331,10 +331,13 @@
|
|
|
331
331
|
* SSE agents were removed with the package). The chip identity, the `@`
|
|
332
332
|
* trigger, the clipboard projection and the menu row are preserved verbatim so
|
|
333
333
|
* existing drafts and the composer UX keep working. The one deliberate change:
|
|
334
|
-
* the chip's model text now
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
334
|
+
* the chip's model text is now the bare clipboard token — the semantics that
|
|
335
|
+
* used to ride in the payload (pin the `kicad-schematic-design` skill instead
|
|
336
|
+
* of the deleted online tools `generate_schematic_from_description` /
|
|
337
|
+
* `generate_system_module_graph`, forbid hand-written KiCad files, defer to
|
|
338
|
+
* the user's words) moved to the node half's `huaqiu:schematic-gen`
|
|
339
|
+
* system-prompt section; see {@link SCHEMATIC_GEN_SERIALIZED} and
|
|
340
|
+
* `src/prompt.ts`.
|
|
338
341
|
*
|
|
339
342
|
* One module owns the identity of the chip, its menu row, and the model text
|
|
340
343
|
* the chip expands to at submit time. Registering the source is MANDATORY, not
|
|
@@ -344,6 +347,12 @@
|
|
|
344
347
|
* source"). A chip inserted without a registered source would therefore block
|
|
345
348
|
* the user's message instead of degrading quietly.
|
|
346
349
|
*
|
|
350
|
+
* The registration is also ORDER-sensitive: the trigger service is provided by
|
|
351
|
+
* a plugin whose own dependency chain is longer than ours, so reading it once
|
|
352
|
+
* at `apply()` time can find nothing. The entry therefore declares
|
|
353
|
+
* `inputTriggers` in its `inject` (cordis parks the plugin until the provider
|
|
354
|
+
* exists) and retries briefly if it still arrives late.
|
|
355
|
+
*
|
|
347
356
|
* Registering it also buys the typed path for free: the user can type `@` and
|
|
348
357
|
* pick *Generate schematic* (生成原理图 under a Chinese UI) from a group beside
|
|
349
358
|
* *Files & folders* / *Sessions*.
|
|
@@ -407,20 +416,27 @@
|
|
|
407
416
|
}
|
|
408
417
|
/**
|
|
409
418
|
* Model form of the chip: what the agent actually reads at the chip's position
|
|
410
|
-
* in the user message.
|
|
419
|
+
* in the user message — the clipboard token itself, nothing more.
|
|
420
|
+
*
|
|
421
|
+
* This is the idiomatic mention pattern (DSH's own draft tests serialize a
|
|
422
|
+
* reference to its ref), and it is deliberate on two counts:
|
|
411
423
|
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
424
|
+
* 1. **Whatever `serialize` returns lands in the USER message** — the submit
|
|
425
|
+
* plane splices it over the chip's clipboard range — so a long payload is
|
|
426
|
+
* noise the user reads back in their own transcript on every send.
|
|
427
|
+
* 2. **The token has no semantics of its own**, so the semantics cannot live
|
|
428
|
+
* here; they live in the node half's `huaqiu:schematic-gen` system-prompt
|
|
429
|
+
* section (see `src/prompt.ts`), which defines the token, pins the
|
|
430
|
+
* `kicad-schematic-design` skill, and carries the guardrails.
|
|
415
431
|
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
432
|
+
* History: the chip first shipped a 6-line `<schematic-gen intent=…>` block,
|
|
433
|
+
* then a 3-line trimmed form, and now delegates entirely to the section. The
|
|
434
|
+
* retired plugin pinned deleted online tools here
|
|
435
|
+
* (`generate_schematic_from_description` / `generate_system_module_graph`);
|
|
436
|
+
* nothing in this constant may ever name a tool again — the skill pin is the
|
|
437
|
+
* section's job.
|
|
422
438
|
*/
|
|
423
|
-
const SCHEMATIC_GEN_SERIALIZED =
|
|
439
|
+
const SCHEMATIC_GEN_SERIALIZED = SCHEMATIC_GEN_CLIPBOARD;
|
|
424
440
|
/**
|
|
425
441
|
* The `ReferenceInsert` this source produces on pick (and on button click).
|
|
426
442
|
*
|
|
@@ -476,23 +492,125 @@
|
|
|
476
492
|
};
|
|
477
493
|
}
|
|
478
494
|
/**
|
|
479
|
-
*
|
|
495
|
+
* One registration attempt, reported instead of swallowed.
|
|
496
|
+
*
|
|
497
|
+
* {@link registerSchematicGenSource} is the disposer-only view for callers that
|
|
498
|
+
* do not care; this is the same attempt with the outcome visible, which is what
|
|
499
|
+
* a caller needs to decide whether a chip may be inserted at all — an
|
|
500
|
+
* unregistered source makes every send fail (see the module header).
|
|
480
501
|
*
|
|
481
|
-
* Never throws: a missing service, a duplicate registration (a second live
|
|
482
|
-
* copy of this plugin) or a throwing registry must not take the whole client
|
|
483
|
-
* half down. Without a registered source the sidebar button degrades to
|
|
484
|
-
* inserting nothing rather than producing a chip that would block submission.
|
|
485
502
|
* @param registry - `ctx.get('inputTriggers')`, or undefined.
|
|
486
|
-
* @returns
|
|
503
|
+
* @returns whether the registration landed, plus its disposer.
|
|
487
504
|
*/
|
|
488
|
-
function
|
|
489
|
-
if (registry === void 0 || registry === null || typeof registry.registerSource !== "function") return
|
|
505
|
+
function registerSchematicGenSourceTracked(registry) {
|
|
506
|
+
if (registry === void 0 || registry === null || typeof registry.registerSource !== "function") return {
|
|
507
|
+
registered: false,
|
|
508
|
+
dispose: () => {}
|
|
509
|
+
};
|
|
490
510
|
try {
|
|
491
511
|
const dispose = registry.registerSource(createSchematicGenSource());
|
|
492
|
-
return
|
|
512
|
+
return {
|
|
513
|
+
registered: true,
|
|
514
|
+
dispose: typeof dispose === "function" ? dispose : () => {}
|
|
515
|
+
};
|
|
493
516
|
} catch {
|
|
494
|
-
return
|
|
517
|
+
return {
|
|
518
|
+
registered: true,
|
|
519
|
+
dispose: () => {}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Host timer, or undefined where there is none.
|
|
525
|
+
*
|
|
526
|
+
* The retry is browser-only machinery, and the built bundle is also evaluated
|
|
527
|
+
* in bare hosts (the vm-based card test, a non-DOM worker) where `setInterval`
|
|
528
|
+
* does not exist. Those must degrade to "not registered yet" — a throw here
|
|
529
|
+
* would take the whole client half down, which is the failure mode this module
|
|
530
|
+
* exists to prevent.
|
|
531
|
+
*/
|
|
532
|
+
function hostTimer() {
|
|
533
|
+
if (typeof setInterval !== "function" || typeof clearInterval !== "function") return void 0;
|
|
534
|
+
return {
|
|
535
|
+
set: (fn, ms) => setInterval(fn, ms),
|
|
536
|
+
clear: (handle) => clearInterval(handle)
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Register the source, polling while the trigger service has not activated yet.
|
|
541
|
+
*
|
|
542
|
+
* Why this exists: activation is service-availability driven, and
|
|
543
|
+
* `inputTriggers` is provided by a plugin (`dsh-client-ui-input-trigger`) whose
|
|
544
|
+
* own dependency chain is far longer than ours. A plugin that reads the service
|
|
545
|
+
* exactly once at `apply()` time and finds it absent registers NOTHING —
|
|
546
|
+
* silently. The chip then lands in the composer (insertion only needs
|
|
547
|
+
* `conversation`) and the send dies on
|
|
548
|
+
* `slash: no serializer for reference source "schematic-gen"`, because the
|
|
549
|
+
* controller routes every chip to its owner's codec. Declaring `inputTriggers`
|
|
550
|
+
* in the plugin's `inject` is the primary fix (cordis then parks this plugin
|
|
551
|
+
* until the provider exists); this poll is the belt-and-braces path for a
|
|
552
|
+
* provider that arrives later than that declaration could promise.
|
|
553
|
+
*
|
|
554
|
+
* @param getRegistry - returns the current `ctx.get('inputTriggers')` value.
|
|
555
|
+
* @param hooks - scheduler/trace seams.
|
|
556
|
+
* @returns a live binding whose `registered()` the caller may consult.
|
|
557
|
+
*/
|
|
558
|
+
function registerSchematicGenSourceWhenReady(getRegistry, hooks = {}) {
|
|
559
|
+
const intervalMs = hooks.intervalMs ?? 250;
|
|
560
|
+
const maxAttempts = hooks.maxAttempts ?? 80;
|
|
561
|
+
const traceFn = hooks.trace ?? trace$1;
|
|
562
|
+
const timer = hooks.setTimer !== void 0 && hooks.clearTimer !== void 0 ? {
|
|
563
|
+
set: hooks.setTimer,
|
|
564
|
+
clear: hooks.clearTimer
|
|
565
|
+
} : hostTimer();
|
|
566
|
+
let current = registerSchematicGenSourceTracked(getRegistry());
|
|
567
|
+
let handle;
|
|
568
|
+
let attempts = 0;
|
|
569
|
+
let stopped = false;
|
|
570
|
+
const stopTimer = () => {
|
|
571
|
+
if (handle === void 0 || timer === void 0) return;
|
|
572
|
+
timer.clear(handle);
|
|
573
|
+
handle = void 0;
|
|
574
|
+
};
|
|
575
|
+
const dispose = () => {
|
|
576
|
+
stopped = true;
|
|
577
|
+
stopTimer();
|
|
578
|
+
current.dispose();
|
|
579
|
+
};
|
|
580
|
+
if (current.registered) return {
|
|
581
|
+
registered: () => true,
|
|
582
|
+
dispose
|
|
583
|
+
};
|
|
584
|
+
if (timer === void 0) {
|
|
585
|
+
traceFn("no timer host — source registration not retried");
|
|
586
|
+
return {
|
|
587
|
+
registered: () => false,
|
|
588
|
+
dispose
|
|
589
|
+
};
|
|
495
590
|
}
|
|
591
|
+
traceFn("waiting for inputTriggers", {
|
|
592
|
+
intervalMs,
|
|
593
|
+
maxAttempts
|
|
594
|
+
});
|
|
595
|
+
handle = timer.set(() => {
|
|
596
|
+
if (stopped) return;
|
|
597
|
+
attempts += 1;
|
|
598
|
+
const next = registerSchematicGenSourceTracked(getRegistry());
|
|
599
|
+
if (next.registered) {
|
|
600
|
+
current = next;
|
|
601
|
+
stopTimer();
|
|
602
|
+
traceFn("registered (late)", { attempts });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (attempts >= maxAttempts) {
|
|
606
|
+
stopTimer();
|
|
607
|
+
traceFn("source registration gave up", { attempts });
|
|
608
|
+
}
|
|
609
|
+
}, intervalMs);
|
|
610
|
+
return {
|
|
611
|
+
registered: () => current.registered,
|
|
612
|
+
dispose
|
|
613
|
+
};
|
|
496
614
|
}
|
|
497
615
|
//#endregion
|
|
498
616
|
//#region src/client/sidebar-entry.ts
|
|
@@ -1126,24 +1244,33 @@
|
|
|
1126
1244
|
}, busy ? "处理中…" : "放置到编辑器") : null, h("span", { style: { fontSize: 12 } }, `${result.fileCount} 个工程和库文件`)), result.status !== "ok" ? h("p", null, "存在未渲染页面或预览警告,请结合工具结果检查。") : null, message ? h("p", { role: "alert" }, message) : null);
|
|
1127
1245
|
}
|
|
1128
1246
|
return {
|
|
1129
|
-
inject: ["slots"],
|
|
1247
|
+
inject: ["slots", "inputTriggers"],
|
|
1130
1248
|
apply: (ctx) => {
|
|
1131
1249
|
getHost = () => ctx.get?.("hqEdge");
|
|
1132
1250
|
const stopVisibility = keepDeliveryVisible();
|
|
1133
|
-
const
|
|
1251
|
+
const source = registerSchematicGenSourceWhenReady(() => ctx.get?.("inputTriggers"));
|
|
1134
1252
|
try {
|
|
1135
1253
|
console.debug("[schematic-gen:client] apply", {
|
|
1136
|
-
inputTriggers:
|
|
1254
|
+
inputTriggers: source.registered(),
|
|
1137
1255
|
slots: typeof ctx.slots?.inject === "function"
|
|
1138
1256
|
});
|
|
1139
1257
|
} catch {}
|
|
1140
|
-
const stopRefs = registerSchematicGenSource(triggers);
|
|
1141
1258
|
const chipDeps = {
|
|
1142
1259
|
getConversation: () => ctx.get?.("conversation"),
|
|
1143
1260
|
getSessions: () => ctx.get?.("sessions")
|
|
1144
1261
|
};
|
|
1145
1262
|
const stopEntry = mountSchematicGenSidebarEntry({
|
|
1146
1263
|
onToggle: () => {
|
|
1264
|
+
if (!source.registered()) {
|
|
1265
|
+
try {
|
|
1266
|
+
console.debug("[schematic-gen:chip] click", {
|
|
1267
|
+
ok: false,
|
|
1268
|
+
reason: "source-unregistered"
|
|
1269
|
+
});
|
|
1270
|
+
} catch {}
|
|
1271
|
+
notifyComposer(ctx, "source-unregistered");
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1147
1274
|
const result = insertSchematicChip(chipDeps);
|
|
1148
1275
|
try {
|
|
1149
1276
|
console.debug("[schematic-gen:chip] click", result);
|
|
@@ -1162,7 +1289,7 @@
|
|
|
1162
1289
|
}, Card));
|
|
1163
1290
|
ctx.effect?.(() => () => {
|
|
1164
1291
|
stopVisibility();
|
|
1165
|
-
|
|
1292
|
+
source.dispose();
|
|
1166
1293
|
stopEntry();
|
|
1167
1294
|
if (typeof dispose === "function") dispose();
|
|
1168
1295
|
});
|
|
@@ -1187,6 +1314,7 @@
|
|
|
1187
1314
|
case "no-session": return "Open a conversation first, then click Schematic Gen again.";
|
|
1188
1315
|
case "busy": return "The composer is busy — try again once the current message settles.";
|
|
1189
1316
|
case "conflict": return "The composer changed while inserting — click Schematic Gen again.";
|
|
1317
|
+
case "source-unregistered": return "Schematic Gen is still starting up — click again in a moment.";
|
|
1190
1318
|
default: return "Schematic Gen is unavailable in this session.";
|
|
1191
1319
|
}
|
|
1192
1320
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -415,7 +415,7 @@ declare function resolveConfig(input?: ConfigInput): Config;
|
|
|
415
415
|
//#endregion
|
|
416
416
|
//#region src/index.d.ts
|
|
417
417
|
declare const name = "@huaqiu/dsh-tool-kicad-schematic-gen";
|
|
418
|
-
declare const inject: readonly ["tools", "fs", "sandboxPolicy", "skills", "webServer"];
|
|
418
|
+
declare const inject: readonly ["tools", "fs", "sandboxPolicy", "skills", "webServer", "systemPrompt"];
|
|
419
419
|
declare function apply(ctx: Context, input?: ConfigInput): void;
|
|
420
420
|
//#endregion
|
|
421
421
|
export { Config, apply, inject, name, resolveConfig };
|
package/lib/index.js
CHANGED
|
@@ -7392,6 +7392,95 @@ function registerModuleAnalysis(ctx, config, analyze = createModuleAnalyzer(ctx,
|
|
|
7392
7392
|
return analyze;
|
|
7393
7393
|
}
|
|
7394
7394
|
//#endregion
|
|
7395
|
+
//#region src/prompt.ts
|
|
7396
|
+
/**
|
|
7397
|
+
* The `huaqiu:schematic-gen` system-prompt section.
|
|
7398
|
+
*
|
|
7399
|
+
* Split out from `src/index.ts` so the text and the registration are
|
|
7400
|
+
* unit-testable without a cordis context, tools, or a skill registry — the
|
|
7401
|
+
* same split `@huaqiu/dsh-eda-selection` uses for its `eda:selection` section.
|
|
7402
|
+
*
|
|
7403
|
+
* ── Why a section at all ────────────────────────────────────────────────────
|
|
7404
|
+
* The composer chip's model form is the bare `@schematic-gen` token (see
|
|
7405
|
+
* `client/schematic-ref.ts`): the token is a UI affordance with no semantics
|
|
7406
|
+
* of its own, and whatever `codec.serialize` returns lands in the USER
|
|
7407
|
+
* message — repeating it on every send is noise the user reads back in their
|
|
7408
|
+
* own transcript. The section carries the semantics once, in the system
|
|
7409
|
+
* prompt, where policy text belongs.
|
|
7410
|
+
*
|
|
7411
|
+
* ── Why static, not a per-session provider ──────────────────────────────────
|
|
7412
|
+
* "Once the user chose Schematic Gen, the rest of the conversation continues
|
|
7413
|
+
* that work" is already true without any session state: the token sits in the
|
|
7414
|
+
* conversation history for every later turn, and the section is re-read on
|
|
7415
|
+
* every assembly. A dynamic provider would need a session-state source — the
|
|
7416
|
+
* only candidates are message-history I/O or a client→node signal, and prompt
|
|
7417
|
+
* assembly is on the agent's hot path (`dsh-eda-selection` documents the same
|
|
7418
|
+
* no-I/O rule). So the section is static text gated on the token, not a
|
|
7419
|
+
* per-session flag.
|
|
7420
|
+
*
|
|
7421
|
+
* @module
|
|
7422
|
+
*/
|
|
7423
|
+
/**
|
|
7424
|
+
* Unique prompt-section name.
|
|
7425
|
+
*
|
|
7426
|
+
* Reuses the retired `huaqiu:schematic-gen` name: `dsh-eda-selection` records
|
|
7427
|
+
* that the old section "has moved to the bundled kicad-schematic-design
|
|
7428
|
+
* skill", and this is that section coming back for the chip era — same slot,
|
|
7429
|
+
* same adjacency, no other registration to collide with.
|
|
7430
|
+
*/
|
|
7431
|
+
const HUAQIU_SCHEMATIC_GEN_SECTION = "huaqiu:schematic-gen";
|
|
7432
|
+
/** The chip token the section is gated on — the chip's clipboard projection. */
|
|
7433
|
+
const SCHEMATIC_GEN_TOKEN = "@schematic-gen";
|
|
7434
|
+
/**
|
|
7435
|
+
* The section text: what the `@schematic-gen` token MEANS.
|
|
7436
|
+
*
|
|
7437
|
+
* Static on purpose (see the module header). Four jobs, each mapping to a
|
|
7438
|
+
* failure mode observed with the earlier inline-payload chip:
|
|
7439
|
+
* 1. define the token as an explicit user choice — a bare token alone is an
|
|
7440
|
+
* opaque string the model has no trained semantics for;
|
|
7441
|
+
* 2. pin the `kicad-schematic-design` skill — the retired online tools are
|
|
7442
|
+
* gone, so an unpinned agent may reach for a dead entry point;
|
|
7443
|
+
* 3. forbid hand-written `.kicad_sch` files — asked for "an ESP32 mini", an
|
|
7444
|
+
* LLM will otherwise author KiCad files itself, which KiCad cannot open;
|
|
7445
|
+
* 4. defer requirements to the user's own words — a gesture is not a spec.
|
|
7446
|
+
*
|
|
7447
|
+
* It never claims the token carries content: it says what the token means,
|
|
7448
|
+
* nothing about what the user asked for.
|
|
7449
|
+
*/
|
|
7450
|
+
function schematicGenSectionText() {
|
|
7451
|
+
return [
|
|
7452
|
+
`Schematic Gen: the ${SCHEMATIC_GEN_TOKEN} token in a user message is the Schematic Gen chip — the user explicitly chose it for that request. Produce the design with the \`kicad-schematic-design\` skill, which starts from the hardware design brief when none exists yet.`,
|
|
7453
|
+
"Never hand-write .kicad_sch files or KiCad project files yourself; go through the skill and its tools.",
|
|
7454
|
+
"Use the user's own words as the design description — if they have not described the circuit yet, ask for the requirements instead of assuming them.",
|
|
7455
|
+
"Follow-up messages in the same conversation continue that work without repeating the token."
|
|
7456
|
+
].join("\n");
|
|
7457
|
+
}
|
|
7458
|
+
/**
|
|
7459
|
+
* Register the static Schematic Gen section.
|
|
7460
|
+
*
|
|
7461
|
+
* Never throws: a DSH deployment without the prompt service, or one where a
|
|
7462
|
+
* duplicate registration races (hot reload), must still load the tools and
|
|
7463
|
+
* the skill — the section is an enhancement, not a dependency of the rest of
|
|
7464
|
+
* the plugin. A duplicate is the expected hot-reload case, so it is swallowed
|
|
7465
|
+
* rather than surfaced; the first registration keeps serving.
|
|
7466
|
+
*
|
|
7467
|
+
* @param systemPrompt - `ctx.systemPrompt`, or undefined.
|
|
7468
|
+
* @returns the section's disposer, or `undefined` when nothing was registered.
|
|
7469
|
+
*/
|
|
7470
|
+
function registerSchematicGenSection(systemPrompt) {
|
|
7471
|
+
if (systemPrompt === void 0 || systemPrompt === null || typeof systemPrompt.section !== "function") return;
|
|
7472
|
+
try {
|
|
7473
|
+
const dispose = systemPrompt.section({
|
|
7474
|
+
name: HUAQIU_SCHEMATIC_GEN_SECTION,
|
|
7475
|
+
order: 590,
|
|
7476
|
+
text: schematicGenSectionText()
|
|
7477
|
+
});
|
|
7478
|
+
return typeof dispose === "function" ? dispose : void 0;
|
|
7479
|
+
} catch {
|
|
7480
|
+
return;
|
|
7481
|
+
}
|
|
7482
|
+
}
|
|
7483
|
+
//#endregion
|
|
7395
7484
|
//#region src/index.ts
|
|
7396
7485
|
const name = "@huaqiu/dsh-tool-kicad-schematic-gen";
|
|
7397
7486
|
const inject = [
|
|
@@ -7399,11 +7488,12 @@ const inject = [
|
|
|
7399
7488
|
"fs",
|
|
7400
7489
|
"sandboxPolicy",
|
|
7401
7490
|
"skills",
|
|
7402
|
-
"webServer"
|
|
7491
|
+
"webServer",
|
|
7492
|
+
"systemPrompt"
|
|
7403
7493
|
];
|
|
7404
7494
|
/** Shared component name for the unified DSH-plugin log (`~/.hq-edge/logs/dsh-plugins`). */
|
|
7405
7495
|
const log = getLogger("dsh-schematic-gen");
|
|
7406
|
-
log.info("schematic-gen: module loaded (waiting for the tools, fs, sandboxPolicy, skills and
|
|
7496
|
+
log.info("schematic-gen: module loaded (waiting for the tools, fs, sandboxPolicy, skills, webServer and systemPrompt services)");
|
|
7407
7497
|
function apply(ctx, input = {}) {
|
|
7408
7498
|
const config = resolveConfig(input);
|
|
7409
7499
|
log.info("applying schematic-gen node half", {
|
|
@@ -7416,6 +7506,9 @@ function apply(ctx, input = {}) {
|
|
|
7416
7506
|
registerModules(ctx, config.modules, analyze);
|
|
7417
7507
|
apply$2(ctx, config.schematic);
|
|
7418
7508
|
apply$1(ctx, config.schematic.kicadCliPath);
|
|
7509
|
+
const disposeSection = registerSchematicGenSection(ctx.systemPrompt);
|
|
7510
|
+
if (disposeSection === void 0) log.warn("systemPrompt service unavailable — the huaqiu:schematic-gen section was not registered");
|
|
7511
|
+
else ctx.effect(() => disposeSection);
|
|
7419
7512
|
} catch (error) {
|
|
7420
7513
|
log.error("schematic-gen node half failed", { error: String(error?.message ?? error) });
|
|
7421
7514
|
throw error;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huaqiu/dsh-tool-kicad-schematic-gen",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "KiCad 原理图设计:电路模块搜索、器件模型与手册、符号导入、编辑预览、脚本构建及设计技能",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"pdfjs-dist": "6.3.289",
|
|
33
33
|
"sharp": "0.35.4",
|
|
34
34
|
"undici": "^8.10.0",
|
|
35
|
-
"@huaqiu/dsh-plugin-log": "0.6.
|
|
36
|
-
"@huaqiu/dsh-sidebar-entry": "0.6.
|
|
35
|
+
"@huaqiu/dsh-plugin-log": "0.6.5",
|
|
36
|
+
"@huaqiu/dsh-sidebar-entry": "0.6.5"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"client": {
|
|
47
47
|
"platform": "web",
|
|
48
48
|
"inject": [
|
|
49
|
-
"@deepseek-ai/dsh-client-runtime"
|
|
49
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
50
|
+
"inputTriggers"
|
|
50
51
|
]
|
|
51
52
|
}
|
|
52
53
|
},
|
package/src/client/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type * as ReactTypes from 'react'
|
|
|
3
3
|
import {parseCard} from './overview-result.ts'
|
|
4
4
|
import {placementHost,placeOverview,type PlacementHost} from './place.ts'
|
|
5
5
|
import {supersededOverviews} from './latest-overview.ts'
|
|
6
|
-
import {
|
|
6
|
+
import {registerSchematicGenSourceWhenReady} from './schematic-ref.ts'
|
|
7
7
|
import {mountSchematicGenSidebarEntry} from './sidebar-entry.ts'
|
|
8
8
|
import {insertSchematicChip,currentSessionId,resolveSessionScope,type ChipInserterDeps,type ConversationLike,type SessionsLike} from './composer-chip.ts'
|
|
9
9
|
|
|
@@ -71,17 +71,21 @@ loader.load({id:'@huaqiu/dsh-tool-kicad-schematic-gen',factory:(require:(name:st
|
|
|
71
71
|
result.status!=='ok'?h('p',null,'存在未渲染页面或预览警告,请结合工具结果检查。'):null,
|
|
72
72
|
message?h('p',{role:'alert'},message):null)
|
|
73
73
|
}
|
|
74
|
-
return {inject:['slots'],apply:(ctx:ClientContext)=>{
|
|
74
|
+
return {inject:['slots','inputTriggers'],apply:(ctx:ClientContext)=>{
|
|
75
75
|
getHost=()=>ctx.get?.('hqEdge') as PlacementHost|undefined
|
|
76
76
|
const stopVisibility=keepDeliveryVisible()
|
|
77
77
|
// Trace the two things that decide whether the plugin is visible at all:
|
|
78
78
|
// the trigger registry (the `@` menu) and the mounted sidebar row. When the
|
|
79
79
|
// node half fails to import, DSH never loads this file — so silence here
|
|
80
80
|
// means "the entry did not activate", not "the row failed to mount".
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
// `inputTriggers` is declared in `inject`, so cordis parks this plugin until
|
|
82
|
+
// the provider exists; `registerSchematicGenSourceWhenReady` still polls, so
|
|
83
|
+
// a provider that arrives later than that promise still gets our source —
|
|
84
|
+
// an unregistered source makes every send fail with
|
|
85
|
+
// `slash: no serializer for reference source "schematic-gen"`.
|
|
86
|
+
const source=registerSchematicGenSourceWhenReady(()=>ctx.get?.('inputTriggers'))
|
|
87
|
+
try{console.debug('[schematic-gen:client] apply',{inputTriggers:source.registered(),
|
|
83
88
|
slots:typeof ctx.slots?.inject==='function'})}catch{}
|
|
84
|
-
const stopRefs=registerSchematicGenSource(triggers)
|
|
85
89
|
// The sidebar toolbutton. Clicking it inserts the chip; it never opens a
|
|
86
90
|
// workspace. Both services are read lazily — they may materialize after this
|
|
87
91
|
// plugin, and a missing one must not take the client half down.
|
|
@@ -90,6 +94,15 @@ loader.load({id:'@huaqiu/dsh-tool-kicad-schematic-gen',factory:(require:(name:st
|
|
|
90
94
|
getSessions:()=>ctx.get?.('sessions') as SessionsLike|undefined,
|
|
91
95
|
}
|
|
92
96
|
const stopEntry=mountSchematicGenSidebarEntry({onToggle:()=>{
|
|
97
|
+
// Never insert a chip DSH cannot submit: the controller routes every chip to
|
|
98
|
+
// its owner's codec, so a source that never registered blocks the whole send
|
|
99
|
+
// instead of degrading. While the trigger pipeline is still warming up, say
|
|
100
|
+
// so and leave the draft clean.
|
|
101
|
+
if(!source.registered()){
|
|
102
|
+
try{console.debug('[schematic-gen:chip] click',{ok:false,reason:'source-unregistered'})}catch{}
|
|
103
|
+
notifyComposer(ctx,'source-unregistered')
|
|
104
|
+
return
|
|
105
|
+
}
|
|
93
106
|
const result=insertSchematicChip(chipDeps)
|
|
94
107
|
// The one line that answers "I clicked and nothing happened": every failure
|
|
95
108
|
// path in the inserter is traced, so the browser console names the reason.
|
|
@@ -101,7 +114,7 @@ loader.load({id:'@huaqiu/dsh-tool-kicad-schematic-gen',factory:(require:(name:st
|
|
|
101
114
|
},position:'after'})
|
|
102
115
|
try{console.debug('[schematic-gen:client] sidebar row',{rows:document.querySelectorAll('[data-hqcg-schematic-entry]').length})}catch{}
|
|
103
116
|
const dispose=ctx.slots.inject('tool.call.toolview',()=>ctx.slots.register({name:'tool.call.toolview',key:'render_kicad_module_preview'},Card))
|
|
104
|
-
ctx.effect?.(()=>()=>{stopVisibility();
|
|
117
|
+
ctx.effect?.(()=>()=>{stopVisibility();source.dispose();stopEntry();if(typeof dispose==='function')dispose()})
|
|
105
118
|
}}
|
|
106
119
|
/** Surface why a chip insertion did not happen, through the composer's own notice channel. Best-effort by design. */
|
|
107
120
|
function notifyComposer(context:ClientContext,reason:string){
|
|
@@ -119,13 +132,16 @@ loader.load({id:'@huaqiu/dsh-tool-kicad-schematic-gen',factory:(require:(name:st
|
|
|
119
132
|
input?.notify?.('info',noticeFor(reason))
|
|
120
133
|
}catch{/* notice delivery is strictly optional */}
|
|
121
134
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
135
|
+
/** Human-readable notice text for one insertion failure reason. */
|
|
136
|
+
function noticeFor(reason:string):string{
|
|
137
|
+
switch(reason){
|
|
138
|
+
case 'no-session':return 'Open a conversation first, then click Schematic Gen again.'
|
|
139
|
+
case 'busy':return 'The composer is busy — try again once the current message settles.'
|
|
140
|
+
case 'conflict':return 'The composer changed while inserting — click Schematic Gen again.'
|
|
141
|
+
// The trigger pipeline has not registered our source yet. Inserting anyway
|
|
142
|
+
// would produce a chip DSH refuses to submit, so the click is declined.
|
|
143
|
+
case 'source-unregistered':return 'Schematic Gen is still starting up — click again in a moment.'
|
|
144
|
+
default:return 'Schematic Gen is unavailable in this session.'
|
|
130
145
|
}
|
|
146
|
+
}
|
|
131
147
|
}})
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
* SSE agents were removed with the package). The chip identity, the `@`
|
|
6
6
|
* trigger, the clipboard projection and the menu row are preserved verbatim so
|
|
7
7
|
* existing drafts and the composer UX keep working. The one deliberate change:
|
|
8
|
-
* the chip's model text now
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* the chip's model text is now the bare clipboard token — the semantics that
|
|
9
|
+
* used to ride in the payload (pin the `kicad-schematic-design` skill instead
|
|
10
|
+
* of the deleted online tools `generate_schematic_from_description` /
|
|
11
|
+
* `generate_system_module_graph`, forbid hand-written KiCad files, defer to
|
|
12
|
+
* the user's words) moved to the node half's `huaqiu:schematic-gen`
|
|
13
|
+
* system-prompt section; see {@link SCHEMATIC_GEN_SERIALIZED} and
|
|
14
|
+
* `src/prompt.ts`.
|
|
12
15
|
*
|
|
13
16
|
* One module owns the identity of the chip, its menu row, and the model text
|
|
14
17
|
* the chip expands to at submit time. Registering the source is MANDATORY, not
|
|
@@ -18,6 +21,12 @@
|
|
|
18
21
|
* source"). A chip inserted without a registered source would therefore block
|
|
19
22
|
* the user's message instead of degrading quietly.
|
|
20
23
|
*
|
|
24
|
+
* The registration is also ORDER-sensitive: the trigger service is provided by
|
|
25
|
+
* a plugin whose own dependency chain is longer than ours, so reading it once
|
|
26
|
+
* at `apply()` time can find nothing. The entry therefore declares
|
|
27
|
+
* `inputTriggers` in its `inject` (cordis parks the plugin until the provider
|
|
28
|
+
* exists) and retries briefly if it still arrives late.
|
|
29
|
+
*
|
|
21
30
|
* Registering it also buys the typed path for free: the user can type `@` and
|
|
22
31
|
* pick *Generate schematic* (生成原理图 under a Chinese UI) from a group beside
|
|
23
32
|
* *Files & folders* / *Sessions*.
|
|
@@ -115,30 +124,27 @@ export function schematicGenDescription(lang: HostLocale | undefined = detectHos
|
|
|
115
124
|
|
|
116
125
|
/**
|
|
117
126
|
* Model form of the chip: what the agent actually reads at the chip's position
|
|
118
|
-
* in the user message.
|
|
127
|
+
* in the user message — the clipboard token itself, nothing more.
|
|
128
|
+
*
|
|
129
|
+
* This is the idiomatic mention pattern (DSH's own draft tests serialize a
|
|
130
|
+
* reference to its ref), and it is deliberate on two counts:
|
|
119
131
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
132
|
+
* 1. **Whatever `serialize` returns lands in the USER message** — the submit
|
|
133
|
+
* plane splices it over the chip's clipboard range — so a long payload is
|
|
134
|
+
* noise the user reads back in their own transcript on every send.
|
|
135
|
+
* 2. **The token has no semantics of its own**, so the semantics cannot live
|
|
136
|
+
* here; they live in the node half's `huaqiu:schematic-gen` system-prompt
|
|
137
|
+
* section (see `src/prompt.ts`), which defines the token, pins the
|
|
138
|
+
* `kicad-schematic-design` skill, and carries the guardrails.
|
|
123
139
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
140
|
+
* History: the chip first shipped a 6-line `<schematic-gen intent=…>` block,
|
|
141
|
+
* then a 3-line trimmed form, and now delegates entirely to the section. The
|
|
142
|
+
* retired plugin pinned deleted online tools here
|
|
143
|
+
* (`generate_schematic_from_description` / `generate_system_module_graph`);
|
|
144
|
+
* nothing in this constant may ever name a tool again — the skill pin is the
|
|
145
|
+
* section's job.
|
|
130
146
|
*/
|
|
131
|
-
export const SCHEMATIC_GEN_SERIALIZED =
|
|
132
|
-
'<schematic-gen intent="kicad-schematic-design">\n' +
|
|
133
|
-
'The user explicitly chose Schematic Gen for this request. Produce the design ' +
|
|
134
|
-
'with the `kicad-schematic-design` skill; when the hardware design brief is ' +
|
|
135
|
-
'missing or incomplete, the skill starts by preparing it before the schematic ' +
|
|
136
|
-
'work.\n' +
|
|
137
|
-
'Never hand-write .kicad_sch files or KiCad project files yourself — always go ' +
|
|
138
|
-
'through the skill and its tools.\n' +
|
|
139
|
-
"Use the user's own words as the design description. If they have not described " +
|
|
140
|
-
'the circuit yet, ask for the requirements instead of assuming them.\n' +
|
|
141
|
-
'</schematic-gen>'
|
|
147
|
+
export const SCHEMATIC_GEN_SERIALIZED = SCHEMATIC_GEN_CLIPBOARD
|
|
142
148
|
|
|
143
149
|
/** Menu/group candidate description. */
|
|
144
150
|
/**
|
|
@@ -255,18 +261,167 @@ export function createSchematicGenSource(
|
|
|
255
261
|
* @returns disposer (a no-op when registration did not happen).
|
|
256
262
|
*/
|
|
257
263
|
export function registerSchematicGenSource(registry: unknown): () => void {
|
|
264
|
+
return registerSchematicGenSourceTracked(registry).dispose
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Outcome of one registration attempt. */
|
|
268
|
+
export interface SchematicGenRegistration {
|
|
269
|
+
/** Whether the source is now live in the trigger roster. */
|
|
270
|
+
readonly registered: boolean
|
|
271
|
+
/** Unregister (a no-op when the attempt did not register). */
|
|
272
|
+
readonly dispose: () => void
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* One registration attempt, reported instead of swallowed.
|
|
277
|
+
*
|
|
278
|
+
* {@link registerSchematicGenSource} is the disposer-only view for callers that
|
|
279
|
+
* do not care; this is the same attempt with the outcome visible, which is what
|
|
280
|
+
* a caller needs to decide whether a chip may be inserted at all — an
|
|
281
|
+
* unregistered source makes every send fail (see the module header).
|
|
282
|
+
*
|
|
283
|
+
* @param registry - `ctx.get('inputTriggers')`, or undefined.
|
|
284
|
+
* @returns whether the registration landed, plus its disposer.
|
|
285
|
+
*/
|
|
286
|
+
export function registerSchematicGenSourceTracked(registry: unknown): SchematicGenRegistration {
|
|
258
287
|
if (
|
|
259
288
|
registry === undefined || registry === null ||
|
|
260
289
|
typeof (registry as InputTriggerRegistryLike).registerSource !== 'function'
|
|
261
290
|
) {
|
|
262
|
-
return () => {}
|
|
291
|
+
return { registered: false, dispose: () => {} }
|
|
263
292
|
}
|
|
264
293
|
try {
|
|
265
294
|
const dispose = (registry as InputTriggerRegistryLike).registerSource(createSchematicGenSource())
|
|
266
|
-
return
|
|
295
|
+
return {
|
|
296
|
+
registered: true,
|
|
297
|
+
dispose: typeof dispose === 'function' ? dispose : () => {},
|
|
298
|
+
}
|
|
267
299
|
} catch {
|
|
268
300
|
// Already registered (duplicate trigger/name) — the existing owner keeps
|
|
269
301
|
// serving our chips, which is the correct outcome.
|
|
270
|
-
return () => {}
|
|
302
|
+
return { registered: true, dispose: () => {} }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Scheduler seam for {@link registerSchematicGenSourceWhenReady} (tests inject fakes). */
|
|
307
|
+
export interface RegistrationRetryHooks {
|
|
308
|
+
/** Poll interval while the trigger service is still absent. */
|
|
309
|
+
intervalMs?: number
|
|
310
|
+
/** Give up after this many polls; default 80 (≈20 s at 250 ms). */
|
|
311
|
+
maxAttempts?: number
|
|
312
|
+
/** Timer constructor; defaults to the host `setInterval`. */
|
|
313
|
+
setTimer?: (fn: () => void, ms: number) => unknown
|
|
314
|
+
/** Timer destructor; defaults to the host `clearInterval`. */
|
|
315
|
+
clearTimer?: (handle: unknown) => void
|
|
316
|
+
/** Trace sink; defaults to this module's {@link trace}. */
|
|
317
|
+
trace?: (event: string, payload?: unknown) => void
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Live registration handle returned by {@link registerSchematicGenSourceWhenReady}. */
|
|
321
|
+
export interface SchematicGenSourceBinding {
|
|
322
|
+
/** Whether the source is registered RIGHT NOW (re-read on every call). */
|
|
323
|
+
registered(): boolean
|
|
324
|
+
/** Stop retrying and unregister. */
|
|
325
|
+
dispose(): void
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Host timer, or undefined where there is none.
|
|
330
|
+
*
|
|
331
|
+
* The retry is browser-only machinery, and the built bundle is also evaluated
|
|
332
|
+
* in bare hosts (the vm-based card test, a non-DOM worker) where `setInterval`
|
|
333
|
+
* does not exist. Those must degrade to "not registered yet" — a throw here
|
|
334
|
+
* would take the whole client half down, which is the failure mode this module
|
|
335
|
+
* exists to prevent.
|
|
336
|
+
*/
|
|
337
|
+
function hostTimer(): { set: (fn: () => void, ms: number) => unknown; clear: (handle: unknown) => void } | undefined {
|
|
338
|
+
if (typeof setInterval !== 'function' || typeof clearInterval !== 'function') return undefined
|
|
339
|
+
return {
|
|
340
|
+
set: (fn, ms) => setInterval(fn, ms),
|
|
341
|
+
clear: (handle) => clearInterval(handle as never),
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Register the source, polling while the trigger service has not activated yet.
|
|
347
|
+
*
|
|
348
|
+
* Why this exists: activation is service-availability driven, and
|
|
349
|
+
* `inputTriggers` is provided by a plugin (`dsh-client-ui-input-trigger`) whose
|
|
350
|
+
* own dependency chain is far longer than ours. A plugin that reads the service
|
|
351
|
+
* exactly once at `apply()` time and finds it absent registers NOTHING —
|
|
352
|
+
* silently. The chip then lands in the composer (insertion only needs
|
|
353
|
+
* `conversation`) and the send dies on
|
|
354
|
+
* `slash: no serializer for reference source "schematic-gen"`, because the
|
|
355
|
+
* controller routes every chip to its owner's codec. Declaring `inputTriggers`
|
|
356
|
+
* in the plugin's `inject` is the primary fix (cordis then parks this plugin
|
|
357
|
+
* until the provider exists); this poll is the belt-and-braces path for a
|
|
358
|
+
* provider that arrives later than that declaration could promise.
|
|
359
|
+
*
|
|
360
|
+
* @param getRegistry - returns the current `ctx.get('inputTriggers')` value.
|
|
361
|
+
* @param hooks - scheduler/trace seams.
|
|
362
|
+
* @returns a live binding whose `registered()` the caller may consult.
|
|
363
|
+
*/
|
|
364
|
+
export function registerSchematicGenSourceWhenReady(
|
|
365
|
+
getRegistry: () => unknown,
|
|
366
|
+
hooks: RegistrationRetryHooks = {},
|
|
367
|
+
): SchematicGenSourceBinding {
|
|
368
|
+
const intervalMs = hooks.intervalMs ?? 250
|
|
369
|
+
const maxAttempts = hooks.maxAttempts ?? 80
|
|
370
|
+
const traceFn = hooks.trace ?? trace
|
|
371
|
+
const timer = hooks.setTimer !== undefined && hooks.clearTimer !== undefined
|
|
372
|
+
? { set: hooks.setTimer, clear: hooks.clearTimer }
|
|
373
|
+
: hostTimer()
|
|
374
|
+
|
|
375
|
+
let current: SchematicGenRegistration = registerSchematicGenSourceTracked(getRegistry())
|
|
376
|
+
let handle: unknown
|
|
377
|
+
let attempts = 0
|
|
378
|
+
let stopped = false
|
|
379
|
+
|
|
380
|
+
const stopTimer = (): void => {
|
|
381
|
+
if (handle === undefined || timer === undefined) return
|
|
382
|
+
timer.clear(handle)
|
|
383
|
+
handle = undefined
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const dispose = (): void => {
|
|
387
|
+
stopped = true
|
|
388
|
+
stopTimer()
|
|
389
|
+
current.dispose()
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (current.registered) {
|
|
393
|
+
return { registered: () => true, dispose }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (timer === undefined) {
|
|
397
|
+
// No timer host: the caller's `registered()` stays false and the sidebar
|
|
398
|
+
// click declines to insert, which is the honest outcome — a chip that
|
|
399
|
+
// cannot be submitted is worse than no chip.
|
|
400
|
+
traceFn('no timer host — source registration not retried')
|
|
401
|
+
return { registered: () => false, dispose }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
traceFn('waiting for inputTriggers', { intervalMs, maxAttempts })
|
|
405
|
+
handle = timer.set(() => {
|
|
406
|
+
if (stopped) return
|
|
407
|
+
attempts += 1
|
|
408
|
+
const next = registerSchematicGenSourceTracked(getRegistry())
|
|
409
|
+
if (next.registered) {
|
|
410
|
+
// The earlier attempt registered nothing, so it carries no disposer of
|
|
411
|
+
// its own — only the winning registration needs unregistering.
|
|
412
|
+
current = next
|
|
413
|
+
stopTimer()
|
|
414
|
+
traceFn('registered (late)', { attempts })
|
|
415
|
+
return
|
|
416
|
+
}
|
|
417
|
+
if (attempts >= maxAttempts) {
|
|
418
|
+
stopTimer()
|
|
419
|
+
traceFn('source registration gave up', { attempts })
|
|
420
|
+
}
|
|
421
|
+
}, intervalMs)
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
registered: () => current.registered,
|
|
425
|
+
dispose,
|
|
271
426
|
}
|
|
272
427
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,10 +8,11 @@ import {apply as registerSchematic} from './schematic/index.ts'
|
|
|
8
8
|
import {apply as registerSkills} from './skills.ts'
|
|
9
9
|
import {registerModuleAnalysis} from './schematic/analysis-tool.ts'
|
|
10
10
|
import {getKiCadPaths} from './runtime-paths.ts'
|
|
11
|
+
import {registerSchematicGenSection} from './prompt.ts'
|
|
11
12
|
|
|
12
13
|
export {Config,resolveConfig} from './config.ts'
|
|
13
14
|
export const name='@huaqiu/dsh-tool-kicad-schematic-gen'
|
|
14
|
-
export const inject=['tools','fs','sandboxPolicy','skills','webServer'] as const
|
|
15
|
+
export const inject=['tools','fs','sandboxPolicy','skills','webServer','systemPrompt'] as const
|
|
15
16
|
|
|
16
17
|
/** Shared component name for the unified DSH-plugin log (`~/.hq-edge/logs/dsh-plugins`). */
|
|
17
18
|
const log = getLogger('dsh-schematic-gen')
|
|
@@ -20,7 +21,7 @@ const log = getLogger('dsh-schematic-gen')
|
|
|
20
21
|
// ready" means the entry is parked on an unprovided service (or that another
|
|
21
22
|
// entry's failure stopped the boot), which is a different problem from a
|
|
22
23
|
// failure inside apply().
|
|
23
|
-
log.info('schematic-gen: module loaded (waiting for the tools, fs, sandboxPolicy, skills and
|
|
24
|
+
log.info('schematic-gen: module loaded (waiting for the tools, fs, sandboxPolicy, skills, webServer and systemPrompt services)')
|
|
24
25
|
|
|
25
26
|
export function apply(ctx:Context, input:ConfigInput={}):void {
|
|
26
27
|
const config=resolveConfig(input)
|
|
@@ -36,6 +37,14 @@ export function apply(ctx:Context, input:ConfigInput={}):void {
|
|
|
36
37
|
registerModules(ctx,config.modules,analyze)
|
|
37
38
|
registerSchematic(ctx,config.schematic)
|
|
38
39
|
registerSkills(ctx,config.schematic.kicadCliPath)
|
|
40
|
+
// The `@schematic-gen` chip serializes to its bare token; this section is
|
|
41
|
+
// what gives that token meaning (see prompt.ts for why it is static).
|
|
42
|
+
const disposeSection = registerSchematicGenSection((ctx as {systemPrompt?:unknown}).systemPrompt)
|
|
43
|
+
if (disposeSection === undefined) {
|
|
44
|
+
log.warn('systemPrompt service unavailable — the huaqiu:schematic-gen section was not registered')
|
|
45
|
+
} else {
|
|
46
|
+
ctx.effect(() => disposeSection)
|
|
47
|
+
}
|
|
39
48
|
} catch (error) {
|
|
40
49
|
log.error('schematic-gen node half failed',{error:String((error as Error)?.message??error)})
|
|
41
50
|
throw error
|
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `huaqiu:schematic-gen` system-prompt section.
|
|
3
|
+
*
|
|
4
|
+
* Split out from `src/index.ts` so the text and the registration are
|
|
5
|
+
* unit-testable without a cordis context, tools, or a skill registry — the
|
|
6
|
+
* same split `@huaqiu/dsh-eda-selection` uses for its `eda:selection` section.
|
|
7
|
+
*
|
|
8
|
+
* ── Why a section at all ────────────────────────────────────────────────────
|
|
9
|
+
* The composer chip's model form is the bare `@schematic-gen` token (see
|
|
10
|
+
* `client/schematic-ref.ts`): the token is a UI affordance with no semantics
|
|
11
|
+
* of its own, and whatever `codec.serialize` returns lands in the USER
|
|
12
|
+
* message — repeating it on every send is noise the user reads back in their
|
|
13
|
+
* own transcript. The section carries the semantics once, in the system
|
|
14
|
+
* prompt, where policy text belongs.
|
|
15
|
+
*
|
|
16
|
+
* ── Why static, not a per-session provider ──────────────────────────────────
|
|
17
|
+
* "Once the user chose Schematic Gen, the rest of the conversation continues
|
|
18
|
+
* that work" is already true without any session state: the token sits in the
|
|
19
|
+
* conversation history for every later turn, and the section is re-read on
|
|
20
|
+
* every assembly. A dynamic provider would need a session-state source — the
|
|
21
|
+
* only candidates are message-history I/O or a client→node signal, and prompt
|
|
22
|
+
* assembly is on the agent's hot path (`dsh-eda-selection` documents the same
|
|
23
|
+
* no-I/O rule). So the section is static text gated on the token, not a
|
|
24
|
+
* per-session flag.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Unique prompt-section name.
|
|
31
|
+
*
|
|
32
|
+
* Reuses the retired `huaqiu:schematic-gen` name: `dsh-eda-selection` records
|
|
33
|
+
* that the old section "has moved to the bundled kicad-schematic-design
|
|
34
|
+
* skill", and this is that section coming back for the chip era — same slot,
|
|
35
|
+
* same adjacency, no other registration to collide with.
|
|
36
|
+
*/
|
|
37
|
+
export const HUAQIU_SCHEMATIC_GEN_SECTION = 'huaqiu:schematic-gen'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Centrally allocated placement: 590, between DSH's `PLAN_POLICY` (500) and
|
|
41
|
+
* `TEAM_POLICY` (600).
|
|
42
|
+
*
|
|
43
|
+
* That is where `dsh-eda-selection` pins its `eda:selection` section, so both
|
|
44
|
+
* Huaqiu policy blocks stay adjacent and both precede the tool-list sections
|
|
45
|
+
* (`TOOL_*` at 1000+) they refer to. `ctx.systemPrompt.getSectionOrder()`
|
|
46
|
+
* resolves only DSH's own named positions, so the number is pinned here and
|
|
47
|
+
* asserted by a test.
|
|
48
|
+
*/
|
|
49
|
+
export const HUAQIU_SCHEMATIC_GEN_SECTION_ORDER = 590
|
|
50
|
+
|
|
51
|
+
/** The chip token the section is gated on — the chip's clipboard projection. */
|
|
52
|
+
export const SCHEMATIC_GEN_TOKEN = '@schematic-gen'
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The section text: what the `@schematic-gen` token MEANS.
|
|
56
|
+
*
|
|
57
|
+
* Static on purpose (see the module header). Four jobs, each mapping to a
|
|
58
|
+
* failure mode observed with the earlier inline-payload chip:
|
|
59
|
+
* 1. define the token as an explicit user choice — a bare token alone is an
|
|
60
|
+
* opaque string the model has no trained semantics for;
|
|
61
|
+
* 2. pin the `kicad-schematic-design` skill — the retired online tools are
|
|
62
|
+
* gone, so an unpinned agent may reach for a dead entry point;
|
|
63
|
+
* 3. forbid hand-written `.kicad_sch` files — asked for "an ESP32 mini", an
|
|
64
|
+
* LLM will otherwise author KiCad files itself, which KiCad cannot open;
|
|
65
|
+
* 4. defer requirements to the user's own words — a gesture is not a spec.
|
|
66
|
+
*
|
|
67
|
+
* It never claims the token carries content: it says what the token means,
|
|
68
|
+
* nothing about what the user asked for.
|
|
69
|
+
*/
|
|
70
|
+
export function schematicGenSectionText(): string {
|
|
71
|
+
return [
|
|
72
|
+
`Schematic Gen: the ${SCHEMATIC_GEN_TOKEN} token in a user message is the ` +
|
|
73
|
+
'Schematic Gen chip — the user explicitly chose it for that request. ' +
|
|
74
|
+
'Produce the design with the `kicad-schematic-design` skill, which ' +
|
|
75
|
+
'starts from the hardware design brief when none exists yet.',
|
|
76
|
+
'Never hand-write .kicad_sch files or KiCad project files yourself; go ' +
|
|
77
|
+
'through the skill and its tools.',
|
|
78
|
+
"Use the user's own words as the design description — if they have not " +
|
|
79
|
+
'described the circuit yet, ask for the requirements instead of ' +
|
|
80
|
+
'assuming them.',
|
|
81
|
+
'Follow-up messages in the same conversation continue that work without ' +
|
|
82
|
+
'repeating the token.',
|
|
83
|
+
].join('\n')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Structural view of `ctx.systemPrompt` — only the face we use.
|
|
88
|
+
*
|
|
89
|
+
* Structural so this module compiles without depending on
|
|
90
|
+
* `@deepseek-ai/dsh-system-prompt` (whose `Context` augmentation would
|
|
91
|
+
* otherwise have to be in the compilation for a typed `ctx.systemPrompt`).
|
|
92
|
+
*/
|
|
93
|
+
export interface SystemPromptLike {
|
|
94
|
+
section(section: {
|
|
95
|
+
name: string
|
|
96
|
+
order: number
|
|
97
|
+
text: string | ((context: unknown) => string)
|
|
98
|
+
}): () => void
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Register the static Schematic Gen section.
|
|
103
|
+
*
|
|
104
|
+
* Never throws: a DSH deployment without the prompt service, or one where a
|
|
105
|
+
* duplicate registration races (hot reload), must still load the tools and
|
|
106
|
+
* the skill — the section is an enhancement, not a dependency of the rest of
|
|
107
|
+
* the plugin. A duplicate is the expected hot-reload case, so it is swallowed
|
|
108
|
+
* rather than surfaced; the first registration keeps serving.
|
|
109
|
+
*
|
|
110
|
+
* @param systemPrompt - `ctx.systemPrompt`, or undefined.
|
|
111
|
+
* @returns the section's disposer, or `undefined` when nothing was registered.
|
|
112
|
+
*/
|
|
113
|
+
export function registerSchematicGenSection(systemPrompt: unknown): (() => void) | undefined {
|
|
114
|
+
if (
|
|
115
|
+
systemPrompt === undefined || systemPrompt === null ||
|
|
116
|
+
typeof (systemPrompt as SystemPromptLike).section !== 'function'
|
|
117
|
+
) {
|
|
118
|
+
return undefined
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const dispose = (systemPrompt as SystemPromptLike).section({
|
|
122
|
+
name: HUAQIU_SCHEMATIC_GEN_SECTION,
|
|
123
|
+
order: HUAQIU_SCHEMATIC_GEN_SECTION_ORDER,
|
|
124
|
+
text: schematicGenSectionText(),
|
|
125
|
+
})
|
|
126
|
+
return typeof dispose === 'function' ? dispose : undefined
|
|
127
|
+
} catch {
|
|
128
|
+
// Duplicate registration (hot reload) — the first one is still live.
|
|
129
|
+
return undefined
|
|
130
|
+
}
|
|
131
|
+
}
|