@huaqiu/dsh-tool-schematic-gen 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.
- package/LICENSE +21 -0
- package/README.md +8 -0
- package/cordis.patch.yml +7 -0
- package/lib/client.js +106004 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.mts +35 -0
- package/lib/index.mjs +1439 -0
- package/package.json +58 -0
- package/src/client/ecad.ts +147 -0
- package/src/client/hit-card.tsx +378 -0
- package/src/client/i18n.ts +148 -0
- package/src/client/index.ts +176 -0
- package/src/client/login-url.ts +91 -0
- package/src/client/parse.ts +153 -0
- package/src/client/progress.ts +161 -0
- package/src/client/stack-frame.tsx +269 -0
- package/src/client/theme.ts +228 -0
- package/src/client/trace-names.ts +308 -0
- package/src/client/trace-tree.ts +185 -0
- package/src/config.ts +203 -0
- package/src/index.ts +130 -0
- package/src/progress.ts +333 -0
- package/src/routes.ts +80 -0
- package/src/sse.ts +448 -0
- package/src/tools.ts +590 -0
- package/src/trace.ts +336 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime locale pack for the schematic/system HIT card (zh + en), default zh.
|
|
3
|
+
*/
|
|
4
|
+
import { useCallback } from 'react'
|
|
5
|
+
import { useLocale } from './theme.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Chinese is the source of truth: `CopyKey` is derived from it and `EN` is
|
|
9
|
+
* typed against it, so a key added to `ZH` but missing from `EN` is a COMPILE
|
|
10
|
+
* error instead of a silent Chinese fallback at runtime.
|
|
11
|
+
*/
|
|
12
|
+
const ZH = {
|
|
13
|
+
'card.title.schematic': '生成原理图',
|
|
14
|
+
'card.title.system': '生成系统设计',
|
|
15
|
+
'card.kind.schematic': '原理图',
|
|
16
|
+
'card.kind.system': '系统设计',
|
|
17
|
+
'card.status.generating': '正在生成…',
|
|
18
|
+
'card.status.generated': '已生成',
|
|
19
|
+
'card.status.failed': '生成失败',
|
|
20
|
+
// Live progress: one label per rung of the coarse stage ladder. The keys
|
|
21
|
+
// match `STAGE_LADDERS` in src/progress.ts; a run can take 10+ minutes, so
|
|
22
|
+
// these are what the user reads while waiting.
|
|
23
|
+
'card.progress.waiting': '正在等待智能体开始…',
|
|
24
|
+
'card.stage.requirement': '解析需求',
|
|
25
|
+
'card.stage.architecture': '设计架构',
|
|
26
|
+
'card.stage.circuit': '设计电路',
|
|
27
|
+
'card.stage.report': '撰写报告',
|
|
28
|
+
'card.stage.output': '导出图纸',
|
|
29
|
+
'card.stage.plan': '制定方案',
|
|
30
|
+
'card.stage.search': '检索器件',
|
|
31
|
+
'card.stage.bom': '生成 BOM',
|
|
32
|
+
'card.stage.modules': '设计模块',
|
|
33
|
+
'card.stage.connect': '连接网络',
|
|
34
|
+
'card.stage.erc': 'ERC 检查',
|
|
35
|
+
'card.stage.export': '导出工程',
|
|
36
|
+
'card.preview.loading': '正在加载预览…',
|
|
37
|
+
'card.preview.renderError': '预览渲染失败:',
|
|
38
|
+
'card.preview.resolveError': '无法解析工件:',
|
|
39
|
+
'card.preview.missing': '没有可用于预览的工件内容',
|
|
40
|
+
'card.error.toolFailed': '工具调用失败',
|
|
41
|
+
'card.error.retry': '重试',
|
|
42
|
+
'card.meta.design': '设计',
|
|
43
|
+
'card.meta.sheets': '{count} 张图纸',
|
|
44
|
+
'card.meta.modules': '{count} 个模块',
|
|
45
|
+
'card.meta.connections': '{count} 条连接',
|
|
46
|
+
'card.action.download': '下载',
|
|
47
|
+
'card.action.downloading': '下载中…',
|
|
48
|
+
'card.action.regenerate': '重新生成',
|
|
49
|
+
'card.action.regenerating': '重新生成中…',
|
|
50
|
+
'card.action.inspect': '详情',
|
|
51
|
+
'card.regeneratePrompt.schematic': '请重新生成刚才的原理图',
|
|
52
|
+
'card.regeneratePrompt.system': '请重新生成刚才的系统设计',
|
|
53
|
+
'card.auth.title': '华秋 EDA AI(eda.cn)登录',
|
|
54
|
+
'card.auth.desc': '工具「{tool}」需要登录华秋 EDA AI 账号才能继续。请在下方的登录框完成登录(或点击左侧「华秋EDA AI登录」按钮);登录完成后,回复助手「已登录,请重试」,助手会自动重新调用该工具。',
|
|
55
|
+
'card.auth.loggedIn': '✓ 已登录{nickname}—— 现在可以回复助手「已登录,请重试」,助手会重新调用工具。',
|
|
56
|
+
'card.auth.loggedOut': '未登录 —— 请在上方登录华秋 EDA AI(eda.cn)账号,或点击左侧「华秋EDA AI 登录」按钮;登录完成后让助手重试。',
|
|
57
|
+
// Substituted into `{nickname}` by `card.auth.loggedIn`. zh uses a
|
|
58
|
+
// full-width colon, en a half-width one plus a space — hardcoding ':'
|
|
59
|
+
// made the English card read "Logged in:John".
|
|
60
|
+
'card.nicknameSep': ':{nickname}',
|
|
61
|
+
} as const
|
|
62
|
+
|
|
63
|
+
export type CopyKey = keyof typeof ZH
|
|
64
|
+
|
|
65
|
+
const EN: Record<CopyKey, string> = {
|
|
66
|
+
'card.title.schematic': 'Generated schematic',
|
|
67
|
+
'card.title.system': 'Generated system design',
|
|
68
|
+
'card.kind.schematic': 'schematic',
|
|
69
|
+
'card.kind.system': 'system design',
|
|
70
|
+
'card.status.generating': 'Generating…',
|
|
71
|
+
'card.status.generated': 'Generated',
|
|
72
|
+
'card.status.failed': 'Failed',
|
|
73
|
+
'card.progress.waiting': 'Waiting for the agent to start…',
|
|
74
|
+
'card.stage.requirement': 'Parsing requirement',
|
|
75
|
+
'card.stage.architecture': 'Designing architecture',
|
|
76
|
+
'card.stage.circuit': 'Designing circuit',
|
|
77
|
+
'card.stage.report': 'Writing report',
|
|
78
|
+
'card.stage.output': 'Exporting sheets',
|
|
79
|
+
'card.stage.plan': 'Planning',
|
|
80
|
+
'card.stage.search': 'Searching parts',
|
|
81
|
+
'card.stage.bom': 'Building BOM',
|
|
82
|
+
'card.stage.modules': 'Designing modules',
|
|
83
|
+
'card.stage.connect': 'Connecting nets',
|
|
84
|
+
'card.stage.erc': 'Running ERC',
|
|
85
|
+
'card.stage.export': 'Exporting project',
|
|
86
|
+
'card.preview.loading': 'Loading preview…',
|
|
87
|
+
'card.preview.renderError': 'Preview render failed: ',
|
|
88
|
+
'card.preview.resolveError': 'Could not resolve artifact: ',
|
|
89
|
+
'card.preview.missing': 'No previewable artifact content',
|
|
90
|
+
'card.error.toolFailed': 'Tool call failed',
|
|
91
|
+
'card.error.retry': 'Retry',
|
|
92
|
+
'card.meta.design': 'Design',
|
|
93
|
+
'card.meta.sheets': '{count} sheet(s)',
|
|
94
|
+
'card.meta.modules': '{count} module(s)',
|
|
95
|
+
'card.meta.connections': '{count} connection(s)',
|
|
96
|
+
'card.action.download': 'Download',
|
|
97
|
+
'card.action.downloading': 'Downloading…',
|
|
98
|
+
'card.action.regenerate': 'Regenerate',
|
|
99
|
+
'card.action.regenerating': 'Regenerating…',
|
|
100
|
+
'card.action.inspect': 'Inspect',
|
|
101
|
+
'card.regeneratePrompt.schematic': 'Please regenerate the schematic above',
|
|
102
|
+
'card.regeneratePrompt.system': 'Please regenerate the system design above',
|
|
103
|
+
'card.auth.title': 'Huaqiu EDA AI (eda.cn) login',
|
|
104
|
+
// The reply phrase was hardcoded to the Chinese "已登录,请重试" even in
|
|
105
|
+
// this English string, telling an English-speaking user to type Chinese.
|
|
106
|
+
'card.auth.desc': 'Tool "{tool}" requires a Huaqiu EDA AI login. Complete the login below (or use the 华秋EDA AI sidebar button); then reply "I have logged in, please retry" so the assistant can retry the tool.',
|
|
107
|
+
'card.auth.loggedIn': '✓ Logged in{nickname} — reply "I have logged in, please retry" and the assistant will retry.',
|
|
108
|
+
'card.auth.loggedOut': 'Not logged in — complete the login above, or use the 华秋EDA AI sidebar button.',
|
|
109
|
+
'card.nicknameSep': ': {nickname}',
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const COPY: Record<'zh' | 'en', Record<CopyKey, string>> = { zh: ZH, en: EN }
|
|
113
|
+
|
|
114
|
+
/** Every copy key, in declaration order (used to assert bilingual balance). */
|
|
115
|
+
export const COPY_KEYS = Object.keys(ZH) as CopyKey[]
|
|
116
|
+
|
|
117
|
+
export type Translate = (key: string, params?: Record<string, unknown>) => string
|
|
118
|
+
|
|
119
|
+
export function translate(lang: 'zh' | 'en', key: string, params?: Record<string, unknown>): string {
|
|
120
|
+
const dict = lang === 'en' ? COPY.en : COPY.zh
|
|
121
|
+
let v = dict[key as CopyKey]
|
|
122
|
+
if (v === undefined) v = COPY.zh[key as CopyKey]
|
|
123
|
+
if (v === undefined) v = key
|
|
124
|
+
if (params) {
|
|
125
|
+
for (const k of Object.keys(params)) {
|
|
126
|
+
v = v.split(`{${k}}`).join(String(params[k]))
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return v
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const defaultT: Translate = (key, params) => translate('zh', key, params)
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Translate bound to the HOST UI language.
|
|
136
|
+
*
|
|
137
|
+
* The card used to hard-pin `const t = defaultT`, which silently froze every
|
|
138
|
+
* string to zh and left the `en` dictionary above as dead code. `useT()`
|
|
139
|
+
* follows the locale store in `theme.js` instead, so the card switches
|
|
140
|
+
* language with the app.
|
|
141
|
+
*/
|
|
142
|
+
export function useT(): Translate {
|
|
143
|
+
const lang = useLocale()
|
|
144
|
+
return useCallback(
|
|
145
|
+
(key: string, params?: Record<string, unknown>) => translate(lang, key, params),
|
|
146
|
+
[lang],
|
|
147
|
+
)
|
|
148
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/dsh-tool-schematic-gen` — browser half (the schematic/system HIT).
|
|
3
|
+
*
|
|
4
|
+
* Registers keyed `tool.call.toolview` entries for both
|
|
5
|
+
* `generate_schematic_from_description` and `generate_system_module_graph`,
|
|
6
|
+
* replacing the stock JSON fallback (and the auth plugin's generic row) with
|
|
7
|
+
* the `GenHit` card: header → summary → ECAD canvas preview (single sheet for
|
|
8
|
+
* the schematic tool, project zip root sheet for the system tool) → actions.
|
|
9
|
+
*
|
|
10
|
+
* The authoritative state comes from DSH itself: the frozen `ToolCallBlock` is
|
|
11
|
+
* projected by the pure `projectToolCall()` into a single phase. Human answers
|
|
12
|
+
* ("regenerate") are sent back to the agent through
|
|
13
|
+
* `sessions.binding(sessionId).session.prompt`, so the node `ask()` stays the
|
|
14
|
+
* single source of truth.
|
|
15
|
+
*/
|
|
16
|
+
import { createElement, useEffect, useState, type ComponentType } from 'react'
|
|
17
|
+
import { GenHit } from './hit-card.jsx'
|
|
18
|
+
import { injectStyles, removeStyles, disposeThemeObserver } from './theme.js'
|
|
19
|
+
import type { AuthStateLike, PromptSender } from './hit-card.jsx'
|
|
20
|
+
|
|
21
|
+
export type { AuthStateLike, PromptSender }
|
|
22
|
+
|
|
23
|
+
const TOOLVIEW_KEYS = [
|
|
24
|
+
'generate_schematic_from_description',
|
|
25
|
+
'generate_system_module_graph',
|
|
26
|
+
] as const
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Client cordis inject: REAL service names only.
|
|
30
|
+
*
|
|
31
|
+
* `huaqiuAuth` is the client service provided by `@huaqiu/dsh-auth`. It is the
|
|
32
|
+
* ONLY sanctioned source of login state — this plugin must not read the
|
|
33
|
+
* credential cache itself (spec §10). Reading `localStorage` directly would
|
|
34
|
+
* duplicate the storage key and the expiry rule, and would report the user as
|
|
35
|
+
* permanently logged out whenever `@huaqiu/dsh-auth` runs in HQ Edge host mode
|
|
36
|
+
* (where there is no browser-side credential cache at all).
|
|
37
|
+
*/
|
|
38
|
+
export const inject: string[] = ['slots', 'sessions', 'huaqiuAuth']
|
|
39
|
+
|
|
40
|
+
/** Minimal structural client context (dsh-client-runtime provides these). */
|
|
41
|
+
export interface ClientContext {
|
|
42
|
+
provide?(name: string, value: unknown): () => void
|
|
43
|
+
slots?: {
|
|
44
|
+
inject(key: string, callback: () => () => void): () => void
|
|
45
|
+
register(spec: { name: string; key?: string; id?: string }, component: unknown): unknown
|
|
46
|
+
}
|
|
47
|
+
get<T = unknown>(name: string): T | undefined
|
|
48
|
+
effect?(fn: () => (() => void) | void, label?: string): void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Structural view of the `huaqiuAuth` CLIENT service (`@huaqiu/dsh-auth`).
|
|
53
|
+
*
|
|
54
|
+
* Declared structurally, never imported: each package must remain
|
|
55
|
+
* independently installable. `nickname` is optional because the HQ Edge host
|
|
56
|
+
* contract may not supply one.
|
|
57
|
+
*/
|
|
58
|
+
export interface HuaqiuAuthClientService {
|
|
59
|
+
auth?: {
|
|
60
|
+
isAuthenticated(): boolean
|
|
61
|
+
getUserInfo(): Promise<{ nickname?: string } | null>
|
|
62
|
+
onAuthStateChanged(listener: (info: { nickname?: string } | null) => void): () => void
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const LOGGED_OUT: AuthStateLike = { authenticated: false }
|
|
67
|
+
|
|
68
|
+
function stateOf(info: { nickname?: string } | null): AuthStateLike {
|
|
69
|
+
if (!info) return LOGGED_OUT
|
|
70
|
+
return {
|
|
71
|
+
authenticated: true,
|
|
72
|
+
...(info.nickname ? { nickname: info.nickname } : {}),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Subscribe to the `huaqiuAuth` client service.
|
|
78
|
+
*
|
|
79
|
+
* Built as a hook rather than a plain read because the service resolves
|
|
80
|
+
* credentials asynchronously (`getUserInfo()`), while the card renders
|
|
81
|
+
* synchronously. The first paint uses the cheap synchronous
|
|
82
|
+
* `isAuthenticated()` so the card never flashes "logged out"; the effect then
|
|
83
|
+
* resolves the full payload once and follows every subsequent change.
|
|
84
|
+
*/
|
|
85
|
+
function createUseAuthState(auth: HuaqiuAuthClientService['auth'] | undefined): () => AuthStateLike {
|
|
86
|
+
return function useAuthState(): AuthStateLike {
|
|
87
|
+
const [state, setState] = useState<AuthStateLike>(() => ({
|
|
88
|
+
authenticated: auth?.isAuthenticated() ?? false,
|
|
89
|
+
}))
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!auth) {
|
|
93
|
+
setState(LOGGED_OUT)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
let alive = true
|
|
97
|
+
void auth.getUserInfo()
|
|
98
|
+
.then((info) => { if (alive) setState(stateOf(info)) })
|
|
99
|
+
.catch(() => { if (alive) setState(LOGGED_OUT) })
|
|
100
|
+
const off = auth.onAuthStateChanged((info) => {
|
|
101
|
+
if (alive) setState(stateOf(info))
|
|
102
|
+
})
|
|
103
|
+
return () => {
|
|
104
|
+
alive = false
|
|
105
|
+
off()
|
|
106
|
+
}
|
|
107
|
+
}, [])
|
|
108
|
+
|
|
109
|
+
return state
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function apply(ctx: ClientContext): () => void {
|
|
114
|
+
const disposers: Array<() => void> = []
|
|
115
|
+
|
|
116
|
+
const authService = ctx.get<HuaqiuAuthClientService | undefined>('huaqiuAuth')
|
|
117
|
+
const useAuthState = createUseAuthState(authService?.auth)
|
|
118
|
+
|
|
119
|
+
const sendPrompt: PromptSender = (sessionId, message) => {
|
|
120
|
+
const sessions = ctx.get<{ binding(id: string): { session: { prompt(content: Array<{ type: 'text'; text: string }>, mode: 'queue'): Promise<unknown> } } | undefined }>('sessions')
|
|
121
|
+
if (!sessions || typeof sessions.binding !== 'function' || !sessionId) {
|
|
122
|
+
return Promise.reject(new Error('no session conversation channel'))
|
|
123
|
+
}
|
|
124
|
+
const binding = sessions.binding(sessionId)
|
|
125
|
+
const session = binding?.session
|
|
126
|
+
if (!session || typeof session.prompt !== 'function') {
|
|
127
|
+
return Promise.reject(new Error('no session conversation channel'))
|
|
128
|
+
}
|
|
129
|
+
return session.prompt([{ type: 'text', text: message }], 'queue')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const slots = ctx.slots
|
|
133
|
+
if (slots && typeof slots.inject === 'function' && typeof slots.register === 'function') {
|
|
134
|
+
const GenHitView: ComponentType<{
|
|
135
|
+
toolName?: string
|
|
136
|
+
block?: unknown
|
|
137
|
+
sessionId?: string
|
|
138
|
+
/**
|
|
139
|
+
* Published as part of `ToolCallOwnerProps` and stable across the
|
|
140
|
+
* running and settled forms. Forwarded so the card can correlate with
|
|
141
|
+
* the node half's progress store.
|
|
142
|
+
*/
|
|
143
|
+
callId?: string
|
|
144
|
+
inspect?: () => void
|
|
145
|
+
}> = (props) =>
|
|
146
|
+
createElement(GenHit, {
|
|
147
|
+
...props,
|
|
148
|
+
toolName: props.toolName ?? '',
|
|
149
|
+
block: props.block as never,
|
|
150
|
+
sessionId: props.sessionId,
|
|
151
|
+
callId: props.callId,
|
|
152
|
+
sendPrompt,
|
|
153
|
+
authState: useAuthState(),
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
for (const toolName of TOOLVIEW_KEYS) {
|
|
157
|
+
disposers.push(slots.inject('tool.call.toolview', () => slots.register(
|
|
158
|
+
{ name: 'tool.call.toolview', key: toolName },
|
|
159
|
+
GenHitView,
|
|
160
|
+
) as () => void))
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
injectStyles()
|
|
165
|
+
|
|
166
|
+
const cleanup = (): void => {
|
|
167
|
+
for (const dispose of disposers) {
|
|
168
|
+
try { dispose() } catch { /* already disposed */ }
|
|
169
|
+
}
|
|
170
|
+
disposers.length = 0
|
|
171
|
+
removeStyles()
|
|
172
|
+
disposeThemeObserver()
|
|
173
|
+
}
|
|
174
|
+
if (ctx.effect) ctx.effect(() => cleanup, 'schematic-gen: HIT dispose')
|
|
175
|
+
return cleanup
|
|
176
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auth.eda.cn embed URL contract for the `needs_auth` login card.
|
|
3
|
+
*
|
|
4
|
+
* SELF-CONTAINED ON PURPOSE: this package must stay independently
|
|
5
|
+
* installable, so it duplicates the ~40 lines of URL contract from
|
|
6
|
+
* `@huaqiu/dsh-auth` (`src/client/lib.ts`) instead of importing them. Keep
|
|
7
|
+
* the two in sync — `AUTH_IFRAME_VERSION` in particular is a shared
|
|
8
|
+
* cache-buster with the web app (`hq-eda-ai` LoginDialog).
|
|
9
|
+
*
|
|
10
|
+
* The embed has two rendering modes and this card uses FILL mode:
|
|
11
|
+
*
|
|
12
|
+
* - **Fill mode** (`fill=full`) — the embed's `DialogContent` becomes
|
|
13
|
+
* `w-full h-full max-w-none max-h-none left-0 top-0 rounded-none border-none`
|
|
14
|
+
* (`eda-cn-login/components/LoginDialog.tsx`, `fillFull` branch) and the
|
|
15
|
+
* page wrapper drops `bg-transparent`, so the embed paints its own
|
|
16
|
+
* `bg-background` edge-to-edge. That is what this card wants: the iframe
|
|
17
|
+
* IS the surface, and fill mode is what removes the white gaps above and
|
|
18
|
+
* below the form that the transparent mode's `grid-rows-[20px_1fr_20px]`
|
|
19
|
+
* strips would otherwise leave showing the host surface.
|
|
20
|
+
* - **Transparent card mode** (no `fill`) — used by the sidebar-triggered
|
|
21
|
+
* login dialog in `dsh-auth`, which paints its own host-colored card
|
|
22
|
+
* around the iframe. Not used here.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const AUTH_ORIGIN = 'https://auth.eda.cn'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Contract version of the auth.eda.cn embed, shared with the web app
|
|
29
|
+
* (`hq-eda-ai` LoginDialog) and `dsh-auth` so all three send the same
|
|
30
|
+
* cache-busting `v=`.
|
|
31
|
+
*/
|
|
32
|
+
export const AUTH_IFRAME_VERSION = '20260409'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Iframe height. Sized so the embed's two-column layout (WeChat QR + phone
|
|
36
|
+
* form, which needs an iframe viewport ≥768px — Tailwind's `md` breakpoint)
|
|
37
|
+
* plus the agreement checkbox fits without an internal scrollbar.
|
|
38
|
+
*/
|
|
39
|
+
export const LOGIN_IFRAME_HEIGHT = 440
|
|
40
|
+
|
|
41
|
+
/** UI language of the auth.eda.cn embed. */
|
|
42
|
+
export type AuthLocale = 'zh' | 'en'
|
|
43
|
+
|
|
44
|
+
/** Color scheme of the auth.eda.cn embed (its own vocabulary). */
|
|
45
|
+
export type AuthTheme = 'light' | 'dark'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* auth.eda.cn's own language ids, keyed by our locale id. The embed's
|
|
49
|
+
* `LanguageContext.getLangFromUrl` only accepts these two ids from
|
|
50
|
+
* `locales/index.ts`; anything else silently falls back to the browser
|
|
51
|
+
* language. `lang=zh` (what hq-eda-ai sends) is ignored for the same reason,
|
|
52
|
+
* but we send it too for parity and forward compatibility.
|
|
53
|
+
*/
|
|
54
|
+
export const AUTH_LOCALE_ID: Record<AuthLocale, string> = { zh: 'cn', en: 'en' }
|
|
55
|
+
|
|
56
|
+
export interface LoginUrlOptions {
|
|
57
|
+
/** Embed UI language (default `zh`). */
|
|
58
|
+
lang?: AuthLocale
|
|
59
|
+
/** Embed color scheme (default `light`). */
|
|
60
|
+
theme?: AuthTheme
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build the auth.eda.cn embed URL for this card: FILL mode, closing on an
|
|
65
|
+
* outside click, in the host's language and color scheme.
|
|
66
|
+
*/
|
|
67
|
+
export function buildLoginUrl(options: LoginUrlOptions = {}): string {
|
|
68
|
+
const url = new URL(`${AUTH_ORIGIN}/`)
|
|
69
|
+
url.searchParams.set('v', AUTH_IFRAME_VERSION)
|
|
70
|
+
url.searchParams.set('clickOutsideToClose', 'true')
|
|
71
|
+
// Fill mode: the card IS the surface, so let the embed paint it.
|
|
72
|
+
url.searchParams.set('fill', 'full')
|
|
73
|
+
const lang = options.lang ?? 'zh'
|
|
74
|
+
url.searchParams.set('locale', AUTH_LOCALE_ID[lang])
|
|
75
|
+
url.searchParams.set('lang', lang)
|
|
76
|
+
url.searchParams.set('theme', options.theme ?? 'light')
|
|
77
|
+
return url.toString()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Background for the iframe ELEMENT, matching the host surface.
|
|
82
|
+
*
|
|
83
|
+
* Even in fill mode the element needs a surface color: Blink's
|
|
84
|
+
* `BaseBackgroundColor()` falls back to WHITE until the embed's own CSS
|
|
85
|
+
* applies, which flashes a white block in dark mode on every card mount.
|
|
86
|
+
* The DSH alias token is preferred so custom host themes are honored, with a
|
|
87
|
+
* per-scheme fallback.
|
|
88
|
+
*/
|
|
89
|
+
export function loginIframeBackground(dark: boolean): string {
|
|
90
|
+
return dark ? 'var(--dsw-alias-bg-layer-1, #20242c)' : 'var(--dsw-alias-bg-layer-1, #ffffff)'
|
|
91
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure projection layer for the schematic/system-design HIT card.
|
|
3
|
+
*
|
|
4
|
+
* Result shapes produced by the node half (`src/tools.ts`):
|
|
5
|
+
* generate_schematic_from_description →
|
|
6
|
+
* { status:'generated', kind:'schematic', design_name, schFiles:[{filename}],
|
|
7
|
+
* schArtifacts:[{id,type,filename,size}], kicadPro, project_achieve_url, note? }
|
|
8
|
+
* generate_system_module_graph →
|
|
9
|
+
* { status:'generated', kind:'system', design_name, module_count,
|
|
10
|
+
* connection_count, module_names, zip_bytes,
|
|
11
|
+
* zipArtifact:{id,type:'zip',filename,size}, note? }
|
|
12
|
+
* both may return { status:'needs_auth', kind, hint }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface ContentBlockLike {
|
|
16
|
+
type?: string
|
|
17
|
+
text?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ToolBlockLike {
|
|
21
|
+
content?: readonly ContentBlockLike[]
|
|
22
|
+
isError?: boolean
|
|
23
|
+
error?: { code?: string } | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ArtifactRef {
|
|
27
|
+
id: string
|
|
28
|
+
type: string | null
|
|
29
|
+
filename: string | null
|
|
30
|
+
size: number | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SchResult {
|
|
34
|
+
status: string | null
|
|
35
|
+
kind: string | null
|
|
36
|
+
artifact: ArtifactRef | null
|
|
37
|
+
designName: string | null
|
|
38
|
+
fileCount: number | null
|
|
39
|
+
moduleCount: number | null
|
|
40
|
+
connectionCount: number | null
|
|
41
|
+
/** User-safe status detail — rendered by the card. */
|
|
42
|
+
note: string | null
|
|
43
|
+
/**
|
|
44
|
+
* Agent-only directive/explanation. Parsed only so the shape is explicit;
|
|
45
|
+
* the card deliberately never renders it.
|
|
46
|
+
*/
|
|
47
|
+
agentNote: string | null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ProjectedPhase =
|
|
51
|
+
| { phase: 'generating' }
|
|
52
|
+
| { phase: 'failed'; message: string }
|
|
53
|
+
| { phase: 'needs_auth'; result: SchResult }
|
|
54
|
+
| { phase: 'completed'; result: SchResult }
|
|
55
|
+
| { phase: 'unknown' }
|
|
56
|
+
|
|
57
|
+
export function resultTextOf(block: ToolBlockLike | undefined): string {
|
|
58
|
+
const content = block?.content
|
|
59
|
+
if (!Array.isArray(content)) return ''
|
|
60
|
+
for (const c of content) {
|
|
61
|
+
if (c && c.type === 'text' && typeof c.text === 'string') return c.text
|
|
62
|
+
}
|
|
63
|
+
return ''
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function firstLine(text: string): string {
|
|
67
|
+
const t = text.trim()
|
|
68
|
+
const nl = t.indexOf('\n')
|
|
69
|
+
return nl === -1 ? t : t.slice(0, nl)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function artOf(a: unknown): ArtifactRef | null {
|
|
73
|
+
if (!a || typeof a !== 'object') return null
|
|
74
|
+
const o = a as Record<string, unknown>
|
|
75
|
+
const id = typeof o.id === 'string' ? o.id : null
|
|
76
|
+
if (!id) return null
|
|
77
|
+
return {
|
|
78
|
+
id,
|
|
79
|
+
type: typeof o.type === 'string' ? o.type : null,
|
|
80
|
+
filename: typeof o.filename === 'string' ? o.filename : null,
|
|
81
|
+
size: typeof o.size === 'number' ? o.size : null,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function parseSchResult(text: string): SchResult | null {
|
|
86
|
+
const t = text.trim()
|
|
87
|
+
if (!t) return null
|
|
88
|
+
let v: unknown
|
|
89
|
+
try {
|
|
90
|
+
v = JSON.parse(t)
|
|
91
|
+
} catch {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) return null
|
|
95
|
+
const o = v as Record<string, unknown>
|
|
96
|
+
|
|
97
|
+
const status = typeof o.status === 'string' ? o.status : null
|
|
98
|
+
const kind = typeof o.kind === 'string' ? o.kind : null
|
|
99
|
+
|
|
100
|
+
let artifact: ArtifactRef | null = null
|
|
101
|
+
if (kind === 'system') {
|
|
102
|
+
artifact = artOf(o.zipArtifact)
|
|
103
|
+
} else {
|
|
104
|
+
const list = Array.isArray(o.schArtifacts) ? (o.schArtifacts as unknown[]) : []
|
|
105
|
+
artifact = list.length > 0 ? artOf(list[0]) : null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
status,
|
|
110
|
+
kind,
|
|
111
|
+
artifact,
|
|
112
|
+
designName: typeof o.design_name === 'string' && o.design_name ? o.design_name : null,
|
|
113
|
+
fileCount: Array.isArray(o.schFiles) ? (o.schFiles as unknown[]).length
|
|
114
|
+
: (Array.isArray(o.schArtifacts) ? (o.schArtifacts as unknown[]).length : null),
|
|
115
|
+
moduleCount: typeof o.module_count === 'number' ? o.module_count : null,
|
|
116
|
+
connectionCount: typeof o.connection_count === 'number' ? o.connection_count : null,
|
|
117
|
+
note: typeof o.note === 'string' ? o.note : null,
|
|
118
|
+
agentNote: typeof o.agentNote === 'string' ? o.agentNote : null,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function projectToolCall(block: ToolBlockLike | undefined): ProjectedPhase {
|
|
123
|
+
if (!block || typeof block !== 'object') return { phase: 'unknown' }
|
|
124
|
+
if (!Array.isArray(block.content)) return { phase: 'generating' }
|
|
125
|
+
if (block.isError === true) {
|
|
126
|
+
const errText = resultTextOf(block)
|
|
127
|
+
const errCode = block.error && typeof block.error.code === 'string' ? block.error.code : null
|
|
128
|
+
return { phase: 'failed', message: firstLine(errText) || (errCode ? `tool error: ${errCode}` : 'generation failed') }
|
|
129
|
+
}
|
|
130
|
+
const text = resultTextOf(block)
|
|
131
|
+
const parsed = parseSchResult(text)
|
|
132
|
+
if (!parsed) return { phase: 'failed', message: firstLine(text) || 'unparseable tool result' }
|
|
133
|
+
if (parsed.status === 'generated') return { phase: 'completed', result: parsed }
|
|
134
|
+
if (parsed.status === 'needs_auth') return { phase: 'needs_auth', result: parsed }
|
|
135
|
+
if (parsed.kind) return { phase: 'completed', result: parsed }
|
|
136
|
+
return { phase: 'failed', message: `unexpected tool status: ${parsed.status}` }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function formatBytes(n: number | null): string {
|
|
140
|
+
if (typeof n !== 'number' || !Number.isFinite(n) || n < 0) return ''
|
|
141
|
+
if (n < 1024) return `${n} B`
|
|
142
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
|
143
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Filename for download: prefer the artifact filename, else derive from kind. */
|
|
147
|
+
export function downloadFilenameFor(kind: string | null, artifact: ArtifactRef | null, designName: string | null): string {
|
|
148
|
+
if (artifact?.filename) return artifact.filename
|
|
149
|
+
const base = designName && designName.length > 0 ? designName : 'generated'
|
|
150
|
+
if (kind === 'system') return `${base}.zip`
|
|
151
|
+
if (kind === 'schematic') return `${base}.kicad_sch`
|
|
152
|
+
return `${base}.txt`
|
|
153
|
+
}
|