@77-zhike/app-sdk 0.2.0-d7b1.13 → 0.2.0-d7b1.14

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/README.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # 智客 App SDK 与 CLI
2
2
 
3
+ ## 操作系统与 Windows 候选
4
+
5
+ 当前 npm 发布版的 Developer CLI 仅提供 macOS Keychain 适配。本开发分支新增 Windows Credential Manager 候选:复用现有浏览器 PKCE 和 Session,使用系统 Windows PowerShell 5.1 固定桥接,refresh credential 按当前用户和 API origin 存储。
6
+
7
+ Windows 候选通过独立的 `SDK Windows 自动化验证(不发布)` Actions 检查,分别运行 Node 22/24。测试制品仅供验证,未发布 npm;Windows 11 桌面、系统默认浏览器弹起、双用户和注销重登暂未覆盖。完整结果与边界见 `openspec/changes/windows-app-developer-cli/`。
8
+
9
+ `zhike doctor --json` 的 `secureStore` 检查表示当前机制可用性,`writeAccess: not_checked` 和 `authentication: not_checked` 不证明凭据已保存、登录有效或业务可用。系统策略禁止桥接时会返回安全存储错误,不修改系统策略或降级为明文文件。
10
+
3
11
  ## 获准依赖
4
12
 
5
13
  工具链从 `src/toolchain/allowed-packages.json` 读取平台维护的包白名单。目前允许 `@77-zhike/qiqi-openapi@0.1.0` 的服务端包根入口 `.`;前端不可导入。项目安装依赖后直接使用静态 ESM import,真实解析的包名、精确版本、入口及间接模块在打包时核对,代码合并进 Artifact。App 的 Manifest、Settings 或 `package.json` 不能扩展名单。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@77-zhike/app-sdk",
3
- "version": "0.2.0-d7b1.13",
3
+ "version": "0.2.0-d7b1.14",
4
4
  "description": "智客 Integration App 一期 SDK 与本地 Toolchain",
5
5
  "repository": {
6
6
  "type": "git",
@@ -67,5 +67,5 @@
67
67
  "optional": true
68
68
  }
69
69
  },
70
- "gitHead": "f2a667ec95a8d1787cf0c0655787266317069b55"
70
+ "gitHead": "04ae9c07742d99322d12790db9c7934cea8f6f64"
71
71
  }
@@ -2,6 +2,7 @@ import { execFile as execFileCallback } from "node:child_process";
2
2
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
3
3
  import { createServer } from "node:http";
4
4
  import { promisify } from "node:util";
5
+ import { win32 } from "node:path";
5
6
 
6
7
  import { CLI_CLIENT_ID } from "../config.js";
7
8
  import { ZhikeCliError } from "../errors.js";
@@ -17,25 +18,33 @@ export async function authorizeCli({
17
18
  openBrowser = openSystemBrowser,
18
19
  onProgress = () => {},
19
20
  timeoutMs = 120_000,
21
+ signal,
20
22
  }) {
21
23
  const state = randomBytes(32).toString("base64url");
22
24
  const codeVerifier = randomBytes(48).toString("base64url");
23
25
  const codeChallenge = createHash("sha256")
24
26
  .update(codeVerifier, "ascii")
25
27
  .digest("base64url");
26
- const callback = await createLoopbackCallback({ state, timeoutMs });
27
- const redirectUri = `http://127.0.0.1:${callback.port}/callback`;
28
- const authorizationUrl = new URL("/cli/authorize", consoleBaseUrl);
29
- authorizationUrl.searchParams.set("client_id", CLI_CLIENT_ID);
30
- authorizationUrl.searchParams.set("redirect_uri", redirectUri);
31
- authorizationUrl.searchParams.set("code_challenge", codeChallenge);
32
- authorizationUrl.searchParams.set("code_challenge_method", "S256");
33
- authorizationUrl.searchParams.set("state", state);
34
-
28
+ const cancellation = new AbortController();
29
+ const stop = () => cancellation.abort();
30
+ const authorizationSignal = signal || cancellation.signal;
31
+ if (!signal) { process.once("SIGINT", stop); process.once("SIGTERM", stop); }
32
+ let callback;
35
33
  try {
34
+ if (authorizationSignal.aborted) throw new ZhikeCliError("zhike_cli_authorization_cancelled", "Developer CLI 授权已取消", { exitCode: 130 });
35
+ callback = await createLoopbackCallback({ state, timeoutMs, signal: authorizationSignal });
36
+ if (authorizationSignal.aborted) await callback.waitForCode;
37
+ const redirectUri = `http://127.0.0.1:${callback.port}/callback`;
38
+ const authorizationUrl = new URL("/cli/authorize", consoleBaseUrl);
39
+ authorizationUrl.searchParams.set("client_id", CLI_CLIENT_ID);
40
+ authorizationUrl.searchParams.set("redirect_uri", redirectUri);
41
+ authorizationUrl.searchParams.set("code_challenge", codeChallenge);
42
+ authorizationUrl.searchParams.set("code_challenge_method", "S256");
43
+ authorizationUrl.searchParams.set("state", state);
36
44
  callback.waitForCode.catch(() => {});
37
45
  onProgress("正在浏览器中完成 Developer CLI 授权...");
38
- await openBrowser(authorizationUrl.toString());
46
+ try { await openBrowser(authorizationUrl.toString()); }
47
+ catch { onProgress(`无法自动打开浏览器,请手动访问:${authorizationUrl}`); }
39
48
  const code = await callback.waitForCode;
40
49
  onProgress("已收到浏览器授权,正在完成 Developer CLI 登录...");
41
50
  const api = new DeveloperApiClient({ apiBaseUrl, fetchImpl });
@@ -45,10 +54,11 @@ export async function authorizeCli({
45
54
  redirect_uri: redirectUri,
46
55
  code_verifier: codeVerifier,
47
56
  });
48
- await credentialStore.set(apiBaseUrl, token.refresh_credential);
57
+ await persistRefreshCredential(api, token.refresh_credential, credentialStore);
49
58
  return renewableClient(api, token.access_token, credentialStore);
50
59
  } finally {
51
- await callback.close();
60
+ await callback?.close();
61
+ if (!signal) { process.removeListener("SIGINT", stop); process.removeListener("SIGTERM", stop); }
52
62
  }
53
63
  }
54
64
 
@@ -77,7 +87,7 @@ export async function authenticateCli({
77
87
  }
78
88
  if (token) {
79
89
  if (token.refresh_credential !== refreshCredential) {
80
- await credentialStore.set(apiBaseUrl, token.refresh_credential);
90
+ await persistRefreshCredential(api, token.refresh_credential, credentialStore);
81
91
  }
82
92
  return renewableClient(api, token.access_token, credentialStore);
83
93
  }
@@ -98,16 +108,27 @@ export async function authenticateCli({
98
108
  });
99
109
  }
100
110
 
101
- export async function openSystemBrowser(url, platform = process.platform) {
111
+ export async function openSystemBrowser(url, platform = process.platform, { runner = execFile, systemRoot = process.env.SystemRoot } = {}) {
112
+ const parsed = new URL(url);
113
+ if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password) {
114
+ throw new ZhikeCliError("zhike_browser_open_failed", "浏览器地址必须是无凭据的 HTTP(S) URL");
115
+ }
116
+ // URL 只作为 base64 数据传入固定表达式,避免 cmd/start 把 query 中的 & 当成命令。
117
+ const encodedUrl = Buffer.from(parsed.href, "utf8").toString("base64");
118
+ const windowsCommand = `Start-Process -FilePath ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedUrl}')))`;
102
119
  const command =
103
120
  platform === "darwin"
104
121
  ? ["open", [url]]
105
122
  : platform === "win32"
106
- ? ["cmd", ["/c", "start", "", url]]
123
+ ? [systemRoot && win32.isAbsolute(systemRoot)
124
+ ? win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "",
125
+ ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(windowsCommand, "utf16le").toString("base64")]]
107
126
  : ["xdg-open", [url]];
108
127
  try {
109
- await execFile(command[0], command[1], {
128
+ if (!command[0]) throw new Error("system browser unavailable");
129
+ await runner(command[0], command[1], {
110
130
  windowsHide: true,
131
+ timeout: 10_000,
111
132
  });
112
133
  } catch (error) {
113
134
  throw new ZhikeCliError(
@@ -127,14 +148,30 @@ function renewableClient(api, accessToken, credentialStore) {
127
148
  }
128
149
  const token = await api.refreshCliSession(refreshCredential);
129
150
  if (token.refresh_credential !== refreshCredential) {
130
- await credentialStore.set(api.apiBaseUrl, token.refresh_credential);
151
+ await persistRefreshCredential(api, token.refresh_credential, credentialStore);
131
152
  }
132
153
  return token.access_token;
133
154
  };
134
155
  return client;
135
156
  }
136
157
 
137
- async function createLoopbackCallback({ state, timeoutMs }) {
158
+ async function persistRefreshCredential(api, refreshCredential, credentialStore) {
159
+ try { await credentialStore.set(api.apiBaseUrl, refreshCredential); }
160
+ catch {
161
+ let cleanupConfirmed = false;
162
+ const previousSignal = api.signal;
163
+ try {
164
+ api.signal = AbortSignal.timeout(5_000);
165
+ await api.logoutCliSession(refreshCredential);
166
+ cleanupConfirmed = true;
167
+ } catch { /* 网络失败不改变本地存储失败这一事实。 */ }
168
+ finally { api.signal = previousSignal; }
169
+ throw new ZhikeCliError("zhike_secure_store_failed", "无法保存 Developer CLI 登录凭据,请修复系统安全存储后重新登录"
170
+ + (cleanupConfirmed ? "" : ";远端会话清理未确认"));
171
+ }
172
+ }
173
+
174
+ async function createLoopbackCallback({ state, timeoutMs, signal }) {
138
175
  let timer;
139
176
  let resolveCode;
140
177
  let rejectCode;
@@ -142,6 +179,7 @@ async function createLoopbackCallback({ state, timeoutMs }) {
142
179
  resolveCode = resolve;
143
180
  rejectCode = reject;
144
181
  });
182
+ waitForCode.catch(() => {});
145
183
  const server = createServer((request, response) => {
146
184
  const url = new URL(request.url || "/", "http://127.0.0.1");
147
185
  const receivedState = url.searchParams.get("state") || "";
@@ -169,6 +207,13 @@ async function createLoopbackCallback({ state, timeoutMs }) {
169
207
  server.once("error", reject);
170
208
  server.listen(0, "127.0.0.1", resolve);
171
209
  });
210
+ const cancel = () => {
211
+ rejectCode(new ZhikeCliError("zhike_cli_authorization_cancelled", "Developer CLI 授权已取消", { exitCode: 130 }));
212
+ server.close();
213
+ server.closeAllConnections();
214
+ };
215
+ signal?.addEventListener("abort", cancel, { once: true });
216
+ if (signal?.aborted) cancel();
172
217
  const address = server.address();
173
218
  if (!address || typeof address === "string") {
174
219
  server.close();
@@ -190,6 +235,7 @@ async function createLoopbackCallback({ state, timeoutMs }) {
190
235
  waitForCode,
191
236
  close: async () => {
192
237
  clearTimeout(timer);
238
+ signal?.removeEventListener("abort", cancel);
193
239
  if (!server.listening) {
194
240
  return;
195
241
  }
@@ -1,7 +1,9 @@
1
1
  import { execFile as execFileCallback } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
+ import { randomUUID } from "node:crypto";
3
4
 
4
5
  import { ZhikeCliError } from "../errors.js";
6
+ import { WindowsCredentialStore } from "./windows-credential-store.js";
5
7
 
6
8
  const execFile = promisify(execFileCallback);
7
9
  const KEYCHAIN_SERVICE = "com.77hub.zhike.developer-cli";
@@ -86,12 +88,31 @@ export function createSystemCredentialStore(platform = process.platform) {
86
88
  if (platform === "darwin") {
87
89
  return new MacOSKeychainCredentialStore();
88
90
  }
91
+ if (platform === "win32") {
92
+ return new WindowsCredentialStore();
93
+ }
89
94
  throw new ZhikeCliError(
90
95
  "zhike_secure_store_unavailable",
91
96
  "当前操作系统尚未配置安全凭据存储适配器",
92
97
  );
93
98
  }
94
99
 
100
+ export async function inspectSystemCredentialStore(platform = process.platform, createStore = createSystemCredentialStore) {
101
+ const provider = platform === "darwin" ? "macos-keychain" : platform === "win32" ? "windows-credential-manager" : null;
102
+ const check = { name: "secureStore", platform, provider, supported: Boolean(provider),
103
+ available: false, ok: false, writeAccess: "not_checked" };
104
+ if (!provider) return { ...check, code: "zhike_secure_store_unavailable" };
105
+ try {
106
+ const store = createStore(platform);
107
+ if (store.probe) await store.probe();
108
+ else await store.get(`https://probe-${randomUUID()}.invalid`);
109
+ return { ...check, available: true, ok: true };
110
+ } catch (error) {
111
+ return { ...check, code: ["zhike_secure_store_unavailable", "zhike_secure_store_failed"].includes(error?.code)
112
+ ? error.code : "zhike_secure_store_failed" };
113
+ }
114
+ }
115
+
95
116
  function _keychainError(error) {
96
117
  return new ZhikeCliError(
97
118
  "zhike_secure_store_failed",
@@ -5,6 +5,7 @@ import { resolve } from "node:path";
5
5
  import { loadCliConfig } from "../config.js";
6
6
  import { ZhikeCliError } from "../errors.js";
7
7
  import { readProjectBinding } from "../project-binding.js";
8
+ import { inspectSystemCredentialStore } from "./credential-store.js";
8
9
 
9
10
  const metadata = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
10
11
  export const commandHelp = {
@@ -72,7 +73,7 @@ export async function runLocalCommand(command, args, { cwd, environment }) {
72
73
  } catch {
73
74
  checks.push({ name: "package", ok: false, code: "package_missing_or_invalid" });
74
75
  }
75
- checks.push({ name: "secureStore", ok: process.platform === "darwin", platform: process.platform });
76
+ checks.push(await inspectSystemCredentialStore());
76
77
  const result = localEnvelope({ localReady: checks.every((check) => check.ok),
77
78
  authentication: "not_checked", installation: "not_checked", checks });
78
79
  result.target = config ? { environment: config.apiBaseUrl, appId: binding?.appId ?? null } : null;
@@ -0,0 +1,131 @@
1
+ $ErrorActionPreference = 'Stop'
2
+ $ProgressPreference = 'SilentlyContinue'
3
+ $PSModuleAutoLoadingPreference = 'None'
4
+
5
+ try {
6
+ if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') {
7
+ [Console]::Out.Write('{"version":1,"ok":false,"code":"zhike_secure_store_unavailable"}')
8
+ exit 0
9
+ }
10
+ Import-Module "$PSHOME\Modules\Microsoft.PowerShell.Utility\Microsoft.PowerShell.Utility.psd1" -ErrorAction Stop
11
+ [Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false)
12
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
13
+ $request = [Console]::In.ReadToEnd() | ConvertFrom-Json
14
+ $keys = @($request.PSObject.Properties.Name)
15
+ if ($request.version -ne 1 -or $request.operation -notin @('get', 'set', 'delete', 'probe') -or
16
+ @($keys | Where-Object { $_ -notin @('version', 'operation', 'origin', 'credential') }).Count -ne 0 -or
17
+ $request.origin -isnot [string] -or $request.origin.Length -gt 2048) { throw 'invalid request' }
18
+ $uri = New-Object System.Uri($request.origin, [System.UriKind]::Absolute)
19
+ if ($uri.Scheme -notin @('http', 'https') -or $uri.UserInfo -or $uri.AbsolutePath -ne '/' -or $uri.Query -or $uri.Fragment) { throw 'invalid origin' }
20
+ if ($request.operation -eq 'set') {
21
+ if ($request.credential -isnot [string] -or $request.credential.Length -lt 1 -or $request.credential.Length -gt 512) { throw 'invalid credential' }
22
+ } elseif ($keys -contains 'credential') { throw 'unexpected credential' }
23
+
24
+ # 仅绑定固定 Win32 API;全部在内存中构造,避免运行时编译器对 Unicode TEMP 的依赖。
25
+ $assembly = [AppDomain]::CurrentDomain.DefineDynamicAssembly(
26
+ (New-Object Reflection.AssemblyName('ZhikeCredentialBridge')), [Reflection.Emit.AssemblyBuilderAccess]::Run)
27
+ $module = $assembly.DefineDynamicModule('ZhikeCredentialBridge')
28
+ $layout = [Reflection.TypeAttributes]::Public -bor [Reflection.TypeAttributes]::SequentialLayout -bor [Reflection.TypeAttributes]::Sealed
29
+ $structure = $module.DefineType('ZhikeCredential', $layout, [ValueType])
30
+ # 字段顺序对应 CREDENTIALW;指针由当前进程位数决定,FILETIME 作为 64 位值对齐。
31
+ $fields = @(
32
+ @('Flags', [uint32]), @('Type', [uint32]), @('TargetName', [IntPtr]), @('Comment', [IntPtr]),
33
+ @('LastWritten', [long]), @('CredentialBlobSize', [uint32]), @('CredentialBlob', [IntPtr]),
34
+ @('Persist', [uint32]), @('AttributeCount', [uint32]), @('Attributes', [IntPtr]),
35
+ @('TargetAlias', [IntPtr]), @('UserName', [IntPtr]))
36
+ foreach ($field in $fields) { [void]$structure.DefineField($field[0], $field[1], [Reflection.FieldAttributes]::Public) }
37
+ $credentialType = $structure.CreateType()
38
+ $binding = $module.DefineType('ZhikeNativeCredential', [Reflection.TypeAttributes]::Public)
39
+ $dllImport = [Runtime.InteropServices.DllImportAttribute]
40
+ $importFields = [Reflection.FieldInfo[]]@($dllImport.GetField('EntryPoint'), $dllImport.GetField('CharSet'), $dllImport.GetField('SetLastError'))
41
+ $signatures = @(
42
+ @('Read', 'CredReadW', [bool], [type[]]@([string], [uint32], [uint32], [IntPtr].MakeByRefType())),
43
+ @('Write', 'CredWriteW', [bool], [type[]]@([IntPtr], [uint32])),
44
+ @('Delete', 'CredDeleteW', [bool], [type[]]@([string], [uint32], [uint32])),
45
+ @('Free', 'CredFree', [void], [type[]]@([IntPtr])))
46
+ foreach ($signature in $signatures) {
47
+ $method = $binding.DefineMethod($signature[0], [Reflection.MethodAttributes]'Public, Static, PinvokeImpl', $signature[2], $signature[3])
48
+ $attribute = New-Object Reflection.Emit.CustomAttributeBuilder(
49
+ $dllImport.GetConstructor([type[]]@([string])), [object[]]@('advapi32.dll'),
50
+ $importFields, [object[]]@($signature[1], [Runtime.InteropServices.CharSet]::Unicode, $true))
51
+ $method.SetCustomAttribute($attribute)
52
+ $method.SetImplementationFlags([Reflection.MethodImplAttributes]::PreserveSig)
53
+ }
54
+ $native = $binding.CreateType()
55
+ $hash = [Security.Cryptography.SHA256]::Create()
56
+ try { $digest = [BitConverter]::ToString($hash.ComputeHash([Text.Encoding]::UTF8.GetBytes($request.origin))).Replace('-', '').ToLowerInvariant() }
57
+ finally { $hash.Dispose() }
58
+ $target = 'com.77hub.zhike.developer-cli/v1/' + $digest
59
+ $value = $null
60
+ switch ($request.operation) {
61
+ { $_ -in @('get', 'probe') } {
62
+ $pointer = [IntPtr]::Zero
63
+ if (-not $native::Read($target, 1, 0, [ref]$pointer)) {
64
+ $errorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
65
+ if ($errorCode -ne 1168) { throw 'credential read failed' }
66
+ } else {
67
+ $blob = [IntPtr]::Zero
68
+ $blobSize = 0
69
+ try {
70
+ # 按固定结构的实际偏移读取,避免 PowerShell 在 PtrToStructure 的 Type/Object 重载间歧义。
71
+ $kind = [Runtime.InteropServices.Marshal]::ReadInt32($pointer, [Runtime.InteropServices.Marshal]::OffsetOf($credentialType, 'Type').ToInt32())
72
+ $persist = [Runtime.InteropServices.Marshal]::ReadInt32($pointer, [Runtime.InteropServices.Marshal]::OffsetOf($credentialType, 'Persist').ToInt32())
73
+ $blobSize = [Runtime.InteropServices.Marshal]::ReadInt32($pointer, [Runtime.InteropServices.Marshal]::OffsetOf($credentialType, 'CredentialBlobSize').ToInt32())
74
+ $blob = [Runtime.InteropServices.Marshal]::ReadIntPtr($pointer, [Runtime.InteropServices.Marshal]::OffsetOf($credentialType, 'CredentialBlob').ToInt32())
75
+ # Generic / 本用户本机跨登录持久化;BlobSize 单位为字节,内容为 UTF-16LE。
76
+ if ($kind -ne 1 -or $persist -ne 2 -or $blobSize -lt 1 -or $blobSize -gt 1024 -or
77
+ $blobSize % 2 -ne 0 -or $blob -eq [IntPtr]::Zero) { throw 'invalid credential metadata' }
78
+ if ($request.operation -eq 'get') {
79
+ $value = [Runtime.InteropServices.Marshal]::PtrToStringUni($blob, [int]($blobSize / 2))
80
+ }
81
+ } finally {
82
+ if ($blob -ne [IntPtr]::Zero -and $blobSize -gt 0 -and $blobSize -le 1024) {
83
+ for ($i = 0; $i -lt $blobSize; $i++) { [Runtime.InteropServices.Marshal]::WriteByte($blob, $i, 0) }
84
+ }
85
+ $native::Free($pointer)
86
+ }
87
+ }
88
+ }
89
+ 'set' {
90
+ $bytes = [Text.Encoding]::Unicode.GetBytes($request.credential)
91
+ $blob = [IntPtr]::Zero
92
+ $record = [IntPtr]::Zero
93
+ $namePointer = [IntPtr]::Zero
94
+ $userPointer = [IntPtr]::Zero
95
+ try {
96
+ $blob = [Runtime.InteropServices.Marshal]::AllocHGlobal($bytes.Length)
97
+ [Runtime.InteropServices.Marshal]::Copy($bytes, 0, $blob, $bytes.Length)
98
+ $namePointer = [Runtime.InteropServices.Marshal]::StringToHGlobalUni($target)
99
+ $userPointer = [Runtime.InteropServices.Marshal]::StringToHGlobalUni($request.origin)
100
+ $credential = [Activator]::CreateInstance($credentialType)
101
+ $credential.Type = 1
102
+ $credential.TargetName = $namePointer
103
+ $credential.UserName = $userPointer
104
+ $credential.CredentialBlob = $blob
105
+ $credential.CredentialBlobSize = $bytes.Length
106
+ $credential.Persist = 2
107
+ $record = [Runtime.InteropServices.Marshal]::AllocHGlobal([Runtime.InteropServices.Marshal]::SizeOf($credential))
108
+ [Runtime.InteropServices.Marshal]::StructureToPtr($credential, $record, $false)
109
+ if (-not $native::Write($record, 0)) { throw 'credential write failed' }
110
+ } finally {
111
+ [Array]::Clear($bytes, 0, $bytes.Length)
112
+ if ($blob -ne [IntPtr]::Zero) {
113
+ [Runtime.InteropServices.Marshal]::Copy($bytes, 0, $blob, $bytes.Length)
114
+ [Runtime.InteropServices.Marshal]::FreeHGlobal($blob)
115
+ }
116
+ foreach ($allocated in @($record, $namePointer, $userPointer)) {
117
+ if ($allocated -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::FreeHGlobal($allocated) }
118
+ }
119
+ }
120
+ }
121
+ 'delete' {
122
+ if (-not $native::Delete($target, 1, 0)) {
123
+ $errorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
124
+ if ($errorCode -ne 1168) { throw 'credential delete failed' }
125
+ }
126
+ }
127
+ }
128
+ [Console]::Out.Write((@{ version = 1; ok = $true; value = $value } | ConvertTo-Json -Compress))
129
+ } catch {
130
+ [Console]::Out.Write('{"version":1,"ok":false,"code":"zhike_secure_store_failed"}')
131
+ }
@@ -0,0 +1,112 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { readFile } from "node:fs/promises";
4
+ import { win32 } from "node:path";
5
+
6
+ import { normalizeOrigin } from "../config.js";
7
+ import { ZhikeCliError } from "../errors.js";
8
+
9
+ let script;
10
+ const failure = () => new ZhikeCliError("zhike_secure_store_failed", "无法访问 Windows 安全凭据存储,请检查系统凭据库和 PowerShell 策略后重试");
11
+ const unavailable = () => new ZhikeCliError("zhike_secure_store_unavailable", "Windows 安全凭据适配不可用,需要系统 Windows PowerShell 5.1 及允许的原生 API 调用");
12
+
13
+ export class WindowsCredentialStore {
14
+ constructor({ runner = runWindowsCredentialBridge } = {}) {
15
+ this.runner = runner;
16
+ }
17
+
18
+ async get(environmentId) { return this.request("get", normalizeOrigin(environmentId, "Developer API")); }
19
+
20
+ async set(environmentId, refreshCredential) {
21
+ if (typeof refreshCredential !== "string" || !refreshCredential.length || refreshCredential.length > 512) throw failure();
22
+ await this.request("set", normalizeOrigin(environmentId, "Developer API"), refreshCredential);
23
+ }
24
+
25
+ async delete(environmentId) { await this.request("delete", normalizeOrigin(environmentId, "Developer API")); }
26
+
27
+ async probe() { await this.request("probe", `https://probe-${randomUUID()}.invalid`); }
28
+
29
+ async request(operation, origin, credential) {
30
+ try {
31
+ const result = await this.runner({ version: 1, operation, origin, ...(credential === undefined ? {} : { credential }) });
32
+ if (!result || result.version !== 1 || typeof result.ok !== "boolean" ||
33
+ Object.keys(result).some((key) => !["version", "ok", "value", "code"].includes(key))) throw failure();
34
+ if (!result.ok) {
35
+ if (result.code === "zhike_secure_store_unavailable") throw unavailable();
36
+ throw failure();
37
+ }
38
+ if (operation === "get") {
39
+ if (result.value !== null && (typeof result.value !== "string" || !result.value.length || result.value.length > 512)) throw failure();
40
+ return result.value;
41
+ }
42
+ if (result.value !== null) throw failure();
43
+ } catch (error) {
44
+ if (error instanceof ZhikeCliError) throw error;
45
+ throw failure();
46
+ }
47
+ }
48
+ }
49
+
50
+ /** 固定程序/脚本与秘密数据分离;子进程输出不得原文进入 CLI 错误。 */
51
+ export async function runWindowsCredentialBridge(request, {
52
+ spawnImpl = spawn, systemRoot = process.env.SystemRoot, environment = process.env,
53
+ timeoutMs = 10_000, maxOutputBytes = 65_536,
54
+ } = {}) {
55
+ if (!systemRoot || !win32.isAbsolute(systemRoot)) throw unavailable();
56
+ let source;
57
+ try { source = await (script ??= readFile(new URL("./windows-credential-bridge.ps1", import.meta.url), "utf8")); } catch { throw unavailable(); }
58
+ const executable = win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
59
+ const encoded = Buffer.from(source, "utf16le").toString("base64");
60
+ const env = Object.fromEntries(Object.entries(environment).filter(([key]) =>
61
+ ["systemroot", "windir", "temp", "tmp", "path", "comspec", "systemdrive",
62
+ "userprofile", "appdata", "localappdata", "homedrive", "homepath", "username", "userdomain",
63
+ "programfiles", "programfiles(x86)", "programw6432", "programdata", "allusersprofile",
64
+ "pathext", "computername", "processor_architecture", "processor_identifier", "processor_level",
65
+ "processor_revision", "number_of_processors", "os"].includes(key.toLowerCase())));
66
+ // ConvertFrom-Json / Add-Type 的模块解析只使用系统目录,不加载项目或用户自定义模块。
67
+ // 固定子进程不需要持久化命令发现缓存;避免依赖 runner/用户的缓存路径或锁。
68
+ env.PSModuleAnalysisCachePath = "NUL";
69
+ env.PSModulePath = win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "Modules");
70
+ return new Promise((resolve, reject) => {
71
+ let child;
72
+ let output = "";
73
+ let outputSize = 0;
74
+ let settled = false;
75
+ let timer;
76
+ const finish = (error, result) => {
77
+ if (settled) return;
78
+ settled = true;
79
+ clearTimeout(timer);
80
+ output = "";
81
+ if (error) reject(error); else resolve(result);
82
+ };
83
+ const terminate = () => {
84
+ if (Number.isInteger(child?.pid) && process.platform === "win32") {
85
+ execFile(win32.join(systemRoot, "System32", "taskkill.exe"), ["/PID", String(child.pid), "/T", "/F"],
86
+ { windowsHide: true, timeout: 3_000 }, () => child.kill());
87
+ } else child?.kill();
88
+ };
89
+ try {
90
+ child = spawnImpl(executable, ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded],
91
+ { shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"], env });
92
+ } catch { finish(unavailable()); return; }
93
+ timer = setTimeout(() => { terminate(); finish(failure()); }, timeoutMs);
94
+ const collect = (bytes, keep) => {
95
+ if (settled) return;
96
+ outputSize += Buffer.byteLength(bytes);
97
+ if (outputSize > maxOutputBytes) { terminate(); finish(failure()); return; }
98
+ if (keep) output += bytes;
99
+ };
100
+ child.stdout.setEncoding("utf8");
101
+ child.stdout.on("data", (bytes) => collect(bytes, true));
102
+ child.stderr.on("data", (bytes) => collect(bytes, false));
103
+ child.on("error", () => finish(unavailable()));
104
+ child.stdin.on("error", () => { terminate(); finish(failure()); });
105
+ child.on("close", (code) => {
106
+ if (settled) return;
107
+ if (code !== 0) { finish(failure()); return; }
108
+ try { finish(null, JSON.parse(output)); } catch { finish(failure()); }
109
+ });
110
+ child.stdin.end(JSON.stringify(request), "utf8");
111
+ });
112
+ }
@@ -148,7 +148,7 @@ async function assertTypeScriptProgram(projectDirectory, { sourceFiles, serverFu
148
148
  const diagnostics = ts.getPreEmitDiagnostics(program);
149
149
  if (diagnostics.length === 0) return;
150
150
  const diagnostic = diagnostics[0];
151
- const contractLine = diagnostic.file?.fileName === contractPath
151
+ const contractLine = diagnostic.file && resolve(diagnostic.file.fileName) === contractPath
152
152
  ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start || 0).line : -1;
153
153
  const contractOwner = contractLine > 0 ? handlers[Math.floor((contractLine - 1) / 2)] : null;
154
154
  const location = contractOwner ? `src/${contractOwner.relativePath}(参数/返回类型)` : diagnostic.file
@@ -314,7 +314,7 @@ function createSdkBuildPlugin(projectDirectory, sources, target) {
314
314
  };
315
315
  export const TableCell = Object.defineProperties(props => createElement(ui.TableCell, props), { Avatar: { get: () => ui.Avatar }, Badge: { value: Badge } });
316
316
  export const Table = Object.defineProperties(props => createElement(ui.Table, props), { Header: { get: () => ui.Table.Header }, HeaderCell: { get: () => ui.Table.HeaderCell }, Body: { get: () => ui.Table.Body }, Row: { get: () => ui.Table.Row }, Cell: { value: TableCell } });
317
- export const { ${[...publicComponentNames.filter(name => !["TextBlock", "Badge", "Table", "TableCell"].includes(name)), "Typography", "Widget"].join(", ")} } = ui;
317
+ export const { ${[...publicComponentNames.filter(name => !["TextBlock", "Badge", "Table", "TableCell"].includes(name)), "Typography", "Widget", "useConversation"].join(", ")} } = ui;
318
318
  ` }));
319
319
  }
320
320
  if (target !== "remote-client") buildApi.onResolve({ filter: /^@77-zhike\/app-sdk\/(client|server)$/ }, (args) => ({
@@ -31,3 +31,46 @@ export interface ConversationErrorEvent {
31
31
  turnId?: string;
32
32
  executionId?: string;
33
33
  }
34
+
35
+ import type { JsonValue } from './index.js';
36
+ /** 定位符不授予权限;prepareTurn须从当前场景白名单解析快照。 */
37
+ export interface ConversationReference { id: string; type: string; label: string; value: Record<string, JsonValue>; }
38
+ export interface PreparedConversationReference extends ConversationReference {
39
+ /** App给出的业务修订或内容指纹,不授予权限。 */
40
+ revision: string;
41
+ /** 发送时冻结的白名单业务数据,历史不重新读取对象。 */
42
+ snapshot: JsonValue;
43
+ }
44
+ export interface ConversationTurnInput { context: JsonValue; references: PreparedConversationReference[]; }
45
+ export interface ConversationOptions {
46
+ agentId: string;
47
+ scenario: string;
48
+ /** 会话创建背景,逐轮数据请使用prepareTurn。 */
49
+ context: string;
50
+ prepareTurn?: (input: { text: string; references: ConversationReference[] }) => ConversationTurnInput | Promise<ConversationTurnInput>;
51
+ }
52
+ export type ConversationRootBinding = (ConversationOptions & { controller?: never }) | {
53
+ controller: ConversationController; agentId?: never; scenario?: never; context?: never; prepareTurn?: never;
54
+ };
55
+ export interface ConversationController {
56
+ readonly state: { readonly canSend: boolean; readonly status: 'idle' | 'preparing' | 'running' | 'stopping' | 'unconfirmed'; readonly readOnly: boolean; readonly conversationId: string };
57
+ /** Promise只确认本次输入受理,模型终态由原callback通知。 */
58
+ send(input: { text: string; references?: ConversationReference[] }): Promise<void>;
59
+ readonly composer: {
60
+ setText(text: string, options?: { mode?: 'append' | 'replace' }): Promise<void>;
61
+ addReference(reference: ConversationReference): Promise<void>;
62
+ removeReference(id: string): Promise<void>;
63
+ };
64
+ }
65
+ export interface ToolResultView {
66
+ readonly messageId: string;
67
+ readonly toolCallId: string;
68
+ readonly tool: { kind: 'app'; key: string; name: string };
69
+ readonly result: JsonValue;
70
+ /** 产生此工具结果的那一轮资料,不能当作当前编辑器真值。 */
71
+ readonly turnContext: JsonValue;
72
+ readonly references: readonly PreparedConversationReference[];
73
+ /** 当前绑定不允许继续或操作此结果;App仍须独立校验业务版本。 */
74
+ readonly readOnly: boolean;
75
+ }
76
+ export function useConversation(options: ConversationOptions): ConversationController;
@@ -0,0 +1,82 @@
1
+ import { assertJsonValue } from './validation.js';
2
+
3
+ const bindings = new WeakMap();
4
+ const idle = Object.freeze({ status: 'preparing', canSend: false, readOnly: false, conversationId: '' });
5
+
6
+ /** 只持有Worker内的局部句柄;调用资格始终由Host重新检查。 */
7
+ export function createConversationComponents(React, components, { useInvoke = () => () => Promise.reject(new Error('runtime_host_interaction_unavailable')) } = {}) {
8
+ const Context = React.createContext(null);
9
+ function useConversation(options) {
10
+ const bindingId = React.useId();
11
+ const invoke = useInvoke();
12
+ const latest = React.useRef(options); latest.current = options;
13
+ const [state, setState] = React.useState(idle);
14
+ const handle = React.useMemo(() => {
15
+ const binding = { bindingId: `${bindingId}:${options.agentId}:${options.scenario}`, state: idle, mounted: false,
16
+ options: () => latest.current, publish(value) { binding.state = value; setState(value); } };
17
+ const operate = (operation, payload) => {
18
+ assertJsonValue(payload);
19
+ if (!binding.mounted || !binding.state.conversationId) return Promise.reject(new Error('app_agent_controller_unbound'));
20
+ return invoke('conversation', { bindingId: binding.bindingId, conversationId: binding.state.conversationId, operation, payload });
21
+ };
22
+ const result = Object.freeze({
23
+ get state() { return binding.state; },
24
+ send: input => operate('send', input),
25
+ composer: Object.freeze({
26
+ setText: (text, options = {}) => operate('setText', { text, mode: options.mode ?? 'append' }),
27
+ addReference: reference => operate('addReference', reference),
28
+ removeReference: id => operate('removeReference', { id }),
29
+ }),
30
+ });
31
+ bindings.set(result, binding);
32
+ return result;
33
+ }, [bindingId, options.agentId, options.scenario]);
34
+ // publish的setState只负责让使用state的页面重新渲染,handle引用保持稳定。
35
+ void state;
36
+ return handle;
37
+ }
38
+ const NativeRoot = components.ConversationRoot;
39
+ function Root({ controller, children, ...props }) {
40
+ const internal = useConversation({ agentId: props.agentId, scenario: props.scenario, context: props.context, prepareTurn: props.prepareTurn });
41
+ const selected = controller ?? internal;
42
+ const binding = bindings.get(selected);
43
+ if (!binding || controller && ['agentId', 'scenario', 'context', 'prepareTurn'].some(key => props[key] !== undefined)) throw new Error('app_agent_controller_config_conflict');
44
+ const options = binding.options();
45
+ React.useEffect(() => {
46
+ if (binding.mounted) throw new Error('app_agent_controller_already_bound');
47
+ binding.mounted = true;
48
+ return () => { binding.mounted = false; binding.publish(idle); };
49
+ }, [binding]);
50
+ const onState = React.useCallback(value => { binding.publish(value); }, [binding]);
51
+ const onPrepareTurn = React.useCallback(async input => {
52
+ const prepare = binding.options().prepareTurn;
53
+ return prepare ? await prepare(input) : { context: null, references: input.references.map(item => ({ ...item, revision: 'unspecified', snapshot: item.value })) };
54
+ }, [binding]);
55
+ const { prepareTurn: _prepare, ...safe } = props;
56
+ return React.createElement(Context.Provider, { value: selected }, React.createElement(NativeRoot, {
57
+ ...safe, agentId: options.agentId, scenario: options.scenario, context: options.context ?? '',
58
+ bindingId: binding.bindingId, onState, ...(options.prepareTurn ? { onPrepareTurn } : {}),
59
+ }, children));
60
+ }
61
+ const NativeMessages = components.ConversationMessages;
62
+ function Messages({ toolRenderers, ...props }) {
63
+ React.useContext(Context);
64
+ const [views, setViews] = React.useState([]);
65
+ const onToolViews = React.useCallback(value => setViews(value), []);
66
+ const selected = views.filter(view => typeof toolRenderers?.[view.tool.key] === 'function');
67
+ return React.createElement(NativeMessages, { ...props, ...(toolRenderers ? { onToolViews,
68
+ resultSlots: selected.map(({ messageId, toolCallId }) => ({ messageId, toolCallId })) } : {}) },
69
+ selected.map(view => React.createElement(components.Stack, { key: view.toolCallId }, React.createElement(toolRenderers[view.tool.key], view))));
70
+ }
71
+ function Conversation(props) {
72
+ // 默认组合与组合Root共享同一控制器,老Bundle的Host节点仍然兼容。
73
+ const { placeholder, ...rootProps } = props;
74
+ return React.createElement(Root, rootProps,
75
+ React.createElement(components.Stack, { direction: 'row' }, React.createElement(components.ConversationHistory), React.createElement(components.ConversationNewButton, { iconOnly: true }), React.createElement(components.ConversationStopButton)),
76
+ React.createElement(Messages), React.createElement(components.ConversationStatus), React.createElement(components.ConversationComposer, { placeholder }));
77
+ }
78
+ return { useConversation, Conversation: Object.assign(Conversation, { Root, Messages,
79
+ Composer: components.ConversationComposer, History: components.ConversationHistory,
80
+ NewButton: components.ConversationNewButton, StopButton: components.ConversationStopButton, Status: components.ConversationStatus }),
81
+ ConversationRoot: Root, ConversationMessages: Messages };
82
+ }
package/src/ui/factory.js CHANGED
@@ -1,10 +1,11 @@
1
+ import { createConversationComponents } from './conversation.js';
1
2
  import { componentTypes, legacyComponentTypes } from './components.generated.js';
2
3
  import { useNormalizedOptions } from '../forms/normalize-options.js';
3
4
  import { assertExternalUrl } from '../client/external-url.js';
4
5
  import { assertBannerActions, assertJsonValue, assertChartData, assertChartSelection, assertAnalysisViewData } from './validation.js';
5
6
  import { assertNativeStyles } from './native-dom.js';
6
7
 
7
- export function createUiComponents(React, { legacy = false, useOwner = () => null, openExternal = () => Promise.reject(new Error('runtime_host_interaction_unavailable')) } = {}) {
8
+ export function createUiComponents(React, { legacy = false, useOwner = () => null, useInvoke, openExternal = () => Promise.reject(new Error('runtime_host_interaction_unavailable')) } = {}) {
8
9
  const TabValue = React.createContext(null);
9
10
  const components = Object.fromEntries((legacy ? legacyComponentTypes : componentTypes).map(type => [type, function RemoteComponent(props) {
10
11
  if (type === 'NativeDOM' && props.styles !== undefined) assertNativeStyles(props.styles);
@@ -70,5 +71,6 @@ export function createUiComponents(React, { legacy = false, useOwner = () => nul
70
71
  Loading: components.WidgetLoading,
71
72
  Decoration: components.WidgetDecoration,
72
73
  });
74
+ if (components.Conversation) Object.assign(components, createConversationComponents(React, components, { useInvoke }));
73
75
  return components;
74
76
  }
package/src/ui/index.d.ts CHANGED
@@ -8,10 +8,7 @@ export interface ChartData { labels: string[]; series: { id: string; label: stri
8
8
  export type AppIconName = "a-arrow-down" | "a-arrow-up" | "a-large-small" | "accessibility" | "activity" | "activity-square" | "ad" | "air-vent" | "airplay" | "alarm-check" | "alarm-clock" | "alarm-clock-check" | "alarm-clock-minus" | "alarm-clock-off" | "alarm-clock-plus" | "alarm-minus" | "alarm-plus" | "alarm-smoke" | "album" | "alert-circle" | "alert-octagon" | "alert-triangle" | "align-center" | "align-center-horizontal" | "align-center-vertical" | "align-end-horizontal" | "align-end-vertical" | "align-horizontal-distribute-center" | "align-horizontal-distribute-end" | "align-horizontal-distribute-start" | "align-horizontal-justify-center" | "align-horizontal-justify-end" | "align-horizontal-justify-start" | "align-horizontal-space-around" | "align-horizontal-space-between" | "align-justify" | "align-left" | "align-right" | "align-start-horizontal" | "align-start-vertical" | "align-vertical-distribute-center" | "align-vertical-distribute-end" | "align-vertical-distribute-start" | "align-vertical-justify-center" | "align-vertical-justify-end" | "align-vertical-justify-start" | "align-vertical-space-around" | "align-vertical-space-between" | "ambulance" | "ampersand" | "ampersands" | "amphora" | "anchor" | "angle" | "angry" | "annoyed" | "antenna" | "anvil" | "aperture" | "app-window" | "app-window-mac" | "apple" | "archive" | "archive-restore" | "archive-x" | "area-chart" | "armchair" | "arrow-big-down" | "arrow-big-down-dash" | "arrow-big-left" | "arrow-big-left-dash" | "arrow-big-right" | "arrow-big-right-dash" | "arrow-big-up" | "arrow-big-up-dash" | "arrow-down" | "arrow-down-0-1" | "arrow-down-01" | "arrow-down-1-0" | "arrow-down-10" | "arrow-down-a-z" | "arrow-down-az" | "arrow-down-circle" | "arrow-down-from-line" | "arrow-down-left" | "arrow-down-left-from-circle" | "arrow-down-left-from-square" | "arrow-down-left-square" | "arrow-down-narrow-wide" | "arrow-down-right" | "arrow-down-right-from-circle" | "arrow-down-right-from-square" | "arrow-down-right-square" | "arrow-down-square" | "arrow-down-to-dot" | "arrow-down-to-line" | "arrow-down-up" | "arrow-down-wide-narrow" | "arrow-down-z-a" | "arrow-down-za" | "arrow-left" | "arrow-left-circle" | "arrow-left-from-line" | "arrow-left-right" | "arrow-left-square" | "arrow-left-to-line" | "arrow-right" | "arrow-right-circle" | "arrow-right-from-line" | "arrow-right-left" | "arrow-right-square" | "arrow-right-to-line" | "arrow-up" | "arrow-up-0-1" | "arrow-up-01" | "arrow-up-1-0" | "arrow-up-10" | "arrow-up-a-z" | "arrow-up-az" | "arrow-up-circle" | "arrow-up-down" | "arrow-up-from-dot" | "arrow-up-from-line" | "arrow-up-left" | "arrow-up-left-from-circle" | "arrow-up-left-from-square" | "arrow-up-left-square" | "arrow-up-narrow-wide" | "arrow-up-right" | "arrow-up-right-from-circle" | "arrow-up-right-from-square" | "arrow-up-right-square" | "arrow-up-square" | "arrow-up-to-line" | "arrow-up-wide-narrow" | "arrow-up-z-a" | "arrow-up-za" | "arrows-up-from-line" | "asterisk" | "asterisk-square" | "astroid" | "at-sign" | "atom" | "audio-lines" | "audio-lines-off" | "audio-lines-x" | "audio-waveform" | "award" | "axe" | "axis-3-d" | "axis-3d" | "baby" | "backpack" | "badge" | "badge-alert" | "badge-cent" | "badge-check" | "badge-dollar-sign" | "badge-euro" | "badge-help" | "badge-indian-rupee" | "badge-info" | "badge-japanese-yen" | "badge-minus" | "badge-percent" | "badge-plus" | "badge-pound-sterling" | "badge-question-mark" | "badge-russian-ruble" | "badge-swiss-franc" | "badge-turkish-lira" | "badge-x" | "baggage-claim" | "balloon" | "ban" | "banana" | "bandage" | "banknote" | "banknote-arrow-down" | "banknote-arrow-up" | "banknote-check" | "banknote-x" | "bar-chart" | "bar-chart-2" | "bar-chart-3" | "bar-chart-4" | "bar-chart-big" | "bar-chart-horizontal" | "bar-chart-horizontal-big" | "barcode" | "barrel" | "baseline" | "bath" | "battery" | "battery-charging" | "battery-full" | "battery-low" | "battery-medium" | "battery-plus" | "battery-warning" | "beaker" | "bean" | "bean-off" | "bed" | "bed-double" | "bed-single" | "beef" | "beef-off" | "beer" | "beer-off" | "bell" | "bell-check" | "bell-dot" | "bell-electric" | "bell-minus" | "bell-off" | "bell-plus" | "bell-ring" | "between-horizonal-end" | "between-horizonal-start" | "between-horizontal-end" | "between-horizontal-start" | "between-vertical-end" | "between-vertical-start" | "biceps-flexed" | "bike" | "binary" | "binoculars" | "biohazard" | "bird" | "birdhouse" | "bitcoin" | "blend" | "blender" | "blinds" | "blocks" | "bluetooth" | "bluetooth-connected" | "bluetooth-off" | "bluetooth-searching" | "bold" | "bolt" | "bomb" | "bone" | "bone-fracture" | "book" | "book-a" | "book-alert" | "book-audio" | "book-check" | "book-copy" | "book-dashed" | "book-down" | "book-headphones" | "book-heart" | "book-image" | "book-key" | "book-lock" | "book-marked" | "book-minus" | "book-open" | "book-open-check" | "book-open-text" | "book-plus" | "book-search" | "book-template" | "book-text" | "book-type" | "book-up" | "book-up-2" | "book-user" | "book-x" | "bookmark" | "bookmark-check" | "bookmark-minus" | "bookmark-off" | "bookmark-plus" | "bookmark-x" | "boom-box" | "bot" | "bot-message-square" | "bot-off" | "bottle-wine" | "bow-arrow" | "box" | "box-select" | "boxes" | "braces" | "brackets" | "brain" | "brain-circuit" | "brain-cog" | "brick-wall" | "brick-wall-fire" | "brick-wall-shield" | "briefcase" | "briefcase-business" | "briefcase-conveyor-belt" | "briefcase-medical" | "bring-to-front" | "broccoli" | "broom" | "broom-sparkles" | "brush" | "brush-cleaning" | "bubbles" | "bug" | "bug-off" | "bug-play" | "building" | "building-2" | "bus" | "bus-front" | "cable" | "cable-car" | "cake" | "cake-slice" | "calculator" | "calendar" | "calendar-1" | "calendar-arrow-down" | "calendar-arrow-up" | "calendar-check" | "calendar-check-2" | "calendar-clock" | "calendar-cog" | "calendar-days" | "calendar-fold" | "calendar-heart" | "calendar-minus" | "calendar-minus-2" | "calendar-off" | "calendar-plus" | "calendar-plus-2" | "calendar-range" | "calendar-search" | "calendar-sync" | "calendar-x" | "calendar-x-2" | "calendars" | "camera" | "camera-off" | "candlestick-chart" | "candy" | "candy-cane" | "candy-off" | "cannabis" | "cannabis-off" | "captions" | "captions-off" | "car" | "car-battery" | "car-front" | "car-taxi-front" | "caravan" | "card-sim" | "carrot" | "case-lower" | "case-sensitive" | "case-upper" | "cassette-tape" | "cast" | "castle" | "cat" | "cctv" | "cctv-off" | "chart-area" | "chart-bar" | "chart-bar-big" | "chart-bar-decreasing" | "chart-bar-increasing" | "chart-bar-stacked" | "chart-candlestick" | "chart-column" | "chart-column-big" | "chart-column-decreasing" | "chart-column-increasing" | "chart-column-stacked" | "chart-gantt" | "chart-line" | "chart-network" | "chart-no-axes-column" | "chart-no-axes-column-decreasing" | "chart-no-axes-column-increasing" | "chart-no-axes-combined" | "chart-no-axes-gantt" | "chart-pie" | "chart-scatter" | "chart-spline" | "check" | "check-check" | "check-circle" | "check-circle-2" | "check-line" | "check-square" | "check-square-2" | "chef-hat" | "cherry" | "chess-bishop" | "chess-king" | "chess-knight" | "chess-pawn" | "chess-queen" | "chess-rook" | "chevron-down" | "chevron-down-circle" | "chevron-down-square" | "chevron-first" | "chevron-last" | "chevron-left" | "chevron-left-circle" | "chevron-left-square" | "chevron-right" | "chevron-right-circle" | "chevron-right-square" | "chevron-up" | "chevron-up-circle" | "chevron-up-square" | "chevrons-down" | "chevrons-down-up" | "chevrons-left" | "chevrons-left-right" | "chevrons-left-right-ellipsis" | "chevrons-right" | "chevrons-right-left" | "chevrons-up" | "chevrons-up-down" | "church" | "cigarette" | "cigarette-off" | "circle" | "circle-alert" | "circle-arrow-down" | "circle-arrow-left" | "circle-arrow-out-down-left" | "circle-arrow-out-down-right" | "circle-arrow-out-up-left" | "circle-arrow-out-up-right" | "circle-arrow-right" | "circle-arrow-up" | "circle-check" | "circle-check-big" | "circle-chevron-down" | "circle-chevron-left" | "circle-chevron-right" | "circle-chevron-up" | "circle-dashed" | "circle-divide" | "circle-dollar-sign" | "circle-dot" | "circle-dot-dashed" | "circle-ellipsis" | "circle-equal" | "circle-euro" | "circle-fading-arrow-up" | "circle-fading-plus" | "circle-gauge" | "circle-help" | "circle-minus" | "circle-off" | "circle-parking" | "circle-parking-off" | "circle-pause" | "circle-percent" | "circle-pile" | "circle-play" | "circle-plus" | "circle-pound-sterling" | "circle-power" | "circle-question-mark" | "circle-slash" | "circle-slash-2" | "circle-slashed" | "circle-small" | "circle-star" | "circle-stop" | "circle-user" | "circle-user-round" | "circle-x" | "circuit-board" | "citrus" | "clapperboard" | "clipboard" | "clipboard-check" | "clipboard-clock" | "clipboard-copy" | "clipboard-edit" | "clipboard-list" | "clipboard-minus" | "clipboard-paste" | "clipboard-pen" | "clipboard-pen-line" | "clipboard-plus" | "clipboard-signature" | "clipboard-type" | "clipboard-x" | "clock" | "clock-1" | "clock-10" | "clock-11" | "clock-12" | "clock-2" | "clock-3" | "clock-4" | "clock-5" | "clock-6" | "clock-7" | "clock-8" | "clock-9" | "clock-alert" | "clock-arrow-down" | "clock-arrow-left" | "clock-arrow-right" | "clock-arrow-up" | "clock-check" | "clock-fading" | "clock-plus" | "closed-caption" | "cloud" | "cloud-alert" | "cloud-backup" | "cloud-check" | "cloud-cog" | "cloud-download" | "cloud-drizzle" | "cloud-fog" | "cloud-hail" | "cloud-lightning" | "cloud-moon" | "cloud-moon-rain" | "cloud-off" | "cloud-rain" | "cloud-rain-wind" | "cloud-snow" | "cloud-sun" | "cloud-sun-rain" | "cloud-sync" | "cloud-upload" | "cloudy" | "clover" | "club" | "code" | "code-2" | "code-square" | "code-xml" | "coffee" | "cog" | "coins" | "columns" | "columns-2" | "columns-3" | "columns-3-cog" | "columns-4" | "columns-settings" | "combine" | "command" | "compass" | "component" | "computer" | "concierge-bell" | "cone" | "construction" | "contact" | "contact-2" | "contact-round" | "container" | "contrast" | "cookie" | "cooking-pot" | "copy" | "copy-check" | "copy-minus" | "copy-plus" | "copy-slash" | "copy-x" | "copyleft" | "copyright" | "corner-down-left" | "corner-down-right" | "corner-left-down" | "corner-left-up" | "corner-right-down" | "corner-right-up" | "corner-up-left" | "corner-up-right" | "cpu" | "creative-commons" | "credit-card" | "croissant" | "crop" | "cross" | "crosshair" | "crown" | "cuboid" | "cup-soda" | "curly-braces" | "currency" | "cylinder" | "dam" | "database" | "database-arrow-down" | "database-arrow-up" | "database-backup" | "database-check" | "database-minus" | "database-plus" | "database-search" | "database-x" | "database-zap" | "decimals-arrow-left" | "decimals-arrow-right" | "delete" | "dessert" | "diameter" | "diamond" | "diamond-minus" | "diamond-percent" | "diamond-plus" | "dice-1" | "dice-2" | "dice-3" | "dice-4" | "dice-5" | "dice-6" | "dices" | "diff" | "disc" | "disc-2" | "disc-3" | "disc-album" | "divide" | "divide-circle" | "divide-square" | "dna" | "dna-off" | "dock" | "dog" | "dollar-sign" | "donut" | "door-closed" | "door-closed-locked" | "door-open" | "dot" | "dot-square" | "download" | "download-cloud" | "drafting-compass" | "drama" | "drill" | "drone" | "droplet" | "droplet-off" | "droplets" | "drum" | "drumstick" | "dumbbell" | "ear" | "ear-off" | "earth" | "earth-lock" | "eclipse" | "edit" | "edit-2" | "edit-3" | "egg" | "egg-fried" | "egg-off" | "eject" | "ellipse" | "ellipsis" | "ellipsis-vertical" | "equal" | "equal-approximately" | "equal-not" | "equal-square" | "eraser" | "ethernet-port" | "euro" | "ev-charger" | "expand" | "external-link" | "eye" | "eye-closed" | "eye-dashed" | "eye-off" | "face-angry" | "face-expressionless" | "face-grinning" | "face-neutral" | "face-slightly-frowning" | "face-slightly-smiling" | "face-slightly-smiling-plus" | "factory" | "fan" | "fast-forward" | "feather" | "fence" | "ferris-wheel" | "file" | "file-archive" | "file-audio" | "file-audio-2" | "file-axis-3-d" | "file-axis-3d" | "file-badge" | "file-badge-2" | "file-bar-chart" | "file-bar-chart-2" | "file-box" | "file-braces" | "file-braces-corner" | "file-chart-column" | "file-chart-column-increasing" | "file-chart-line" | "file-chart-pie" | "file-check" | "file-check-2" | "file-check-corner" | "file-clock" | "file-code" | "file-code-2" | "file-code-corner" | "file-cog" | "file-cog-2" | "file-diff" | "file-digit" | "file-down" | "file-edit" | "file-exclamation-point" | "file-headphone" | "file-heart" | "file-image" | "file-input" | "file-json" | "file-json-2" | "file-key" | "file-key-2" | "file-line-chart" | "file-lock" | "file-lock-2" | "file-minus" | "file-minus-2" | "file-minus-corner" | "file-music" | "file-output" | "file-pen" | "file-pen-line" | "file-pie-chart" | "file-play" | "file-plus" | "file-plus-2" | "file-plus-corner" | "file-question" | "file-question-mark" | "file-scan" | "file-search" | "file-search-2" | "file-search-corner" | "file-signal" | "file-signature" | "file-sliders" | "file-spreadsheet" | "file-stack" | "file-symlink" | "file-terminal" | "file-text" | "file-type" | "file-type-2" | "file-type-corner" | "file-up" | "file-user" | "file-video" | "file-video-2" | "file-video-camera" | "file-volume" | "file-volume-2" | "file-warning" | "file-x" | "file-x-2" | "file-x-corner" | "files" | "film" | "filter" | "filter-x" | "fingerprint" | "fingerprint-pattern" | "fire-extinguisher" | "fish" | "fish-off" | "fish-symbol" | "fishing-hook" | "fishing-rod" | "flag" | "flag-off" | "flag-triangle-left" | "flag-triangle-right" | "flame" | "flame-kindling" | "flashlight" | "flashlight-off" | "flask-conical" | "flask-conical-off" | "flask-round" | "flip-horizontal" | "flip-horizontal-2" | "flip-vertical" | "flip-vertical-2" | "flower" | "flower-2" | "focus" | "fold-horizontal" | "fold-vertical" | "folder" | "folder-archive" | "folder-bookmark" | "folder-check" | "folder-clock" | "folder-closed" | "folder-code" | "folder-cog" | "folder-cog-2" | "folder-dot" | "folder-down" | "folder-edit" | "folder-git" | "folder-git-2" | "folder-heart" | "folder-input" | "folder-kanban" | "folder-key" | "folder-lock" | "folder-minus" | "folder-open" | "folder-open-dot" | "folder-output" | "folder-pen" | "folder-plus" | "folder-root" | "folder-search" | "folder-search-2" | "folder-symlink" | "folder-sync" | "folder-tree" | "folder-up" | "folder-x" | "folders" | "footprints" | "fork-knife" | "fork-knife-crossed" | "forklift" | "form" | "form-input" | "forward" | "frame" | "frown" | "fuel" | "fullscreen" | "function-square" | "funnel" | "funnel-plus" | "funnel-x" | "gallery-horizontal" | "gallery-horizontal-end" | "gallery-thumbnails" | "gallery-vertical" | "gallery-vertical-end" | "gamepad" | "gamepad-2" | "gamepad-directional" | "gantt-chart" | "gantt-chart-square" | "gauge" | "gauge-circle" | "gavel" | "gem" | "georgian-lari" | "ghost" | "gift" | "git-branch" | "git-branch-minus" | "git-branch-plus" | "git-commit" | "git-commit-horizontal" | "git-commit-vertical" | "git-compare" | "git-compare-arrows" | "git-fork" | "git-graph" | "git-merge" | "git-merge-conflict" | "git-pull-request" | "git-pull-request-arrow" | "git-pull-request-closed" | "git-pull-request-create" | "git-pull-request-create-arrow" | "git-pull-request-draft" | "glass-water" | "glasses" | "globe" | "globe-2" | "globe-check" | "globe-lock" | "globe-off" | "globe-x" | "goal" | "gpu" | "grab" | "graduation-cap" | "grape" | "grid" | "grid-2-x-2" | "grid-2-x-2-check" | "grid-2-x-2-plus" | "grid-2-x-2-x" | "grid-2x2" | "grid-2x2-check" | "grid-2x2-plus" | "grid-2x2-x" | "grid-3-x-3" | "grid-3x2" | "grid-3x3" | "grip" | "grip-horizontal" | "grip-vertical" | "group" | "guitar" | "ham" | "hamburger" | "hammer" | "hand" | "hand-coins" | "hand-fist" | "hand-grab" | "hand-heart" | "hand-helping" | "hand-metal" | "hand-platter" | "handbag" | "handshake" | "hard-drive" | "hard-drive-download" | "hard-drive-upload" | "hard-hat" | "hash" | "hat-glasses" | "haze" | "hd" | "hdmi-port" | "heading" | "heading-1" | "heading-2" | "heading-3" | "heading-4" | "heading-5" | "heading-6" | "headphone-off" | "headphones" | "headset" | "heart" | "heart-crack" | "heart-handshake" | "heart-minus" | "heart-off" | "heart-plus" | "heart-pulse" | "heart-x" | "heater" | "helicopter" | "help-circle" | "helping-hand" | "hexagon" | "highlighter" | "history" | "home" | "hop" | "hop-off" | "hospital" | "hotel" | "hourglass" | "house" | "house-heart" | "house-plug" | "house-plus" | "house-wifi" | "ice-cream" | "ice-cream-2" | "ice-cream-bowl" | "ice-cream-cone" | "id-card" | "id-card-lanyard" | "image" | "image-down" | "image-minus" | "image-off" | "image-play" | "image-plus" | "image-up" | "image-upscale" | "images" | "import" | "inbox" | "indent" | "indent-decrease" | "indent-increase" | "indian-rupee" | "infinity" | "info" | "inspect" | "inspection-panel" | "italic" | "iteration-ccw" | "iteration-cw" | "japanese-yen" | "joystick" | "kanban" | "kanban-square" | "kanban-square-dashed" | "kayak" | "key" | "key-round" | "key-square" | "keyboard" | "keyboard-music" | "keyboard-off" | "lamp" | "lamp-ceiling" | "lamp-desk" | "lamp-floor" | "lamp-wall-down" | "lamp-wall-up" | "land-plot" | "landmark" | "languages" | "laptop" | "laptop-2" | "laptop-minimal" | "laptop-minimal-check" | "lasso" | "lasso-select" | "laugh" | "layer-arrow-down" | "layer-arrow-up" | "layers" | "layers-2" | "layers-3" | "layers-arrow-down" | "layers-arrow-up" | "layers-minus" | "layers-plus" | "layout" | "layout-dashboard" | "layout-freeform" | "layout-grid" | "layout-list" | "layout-panel-left" | "layout-panel-top" | "layout-template" | "leaf" | "leafy-green" | "lectern" | "lens-concave" | "lens-convex" | "letter-text" | "library" | "library-big" | "library-square" | "life-buoy" | "ligature" | "lightbulb" | "lightbulb-off" | "line-chart" | "line-dot-right-horizontal" | "line-squiggle" | "line-style" | "link" | "link-2" | "link-2-off" | "list" | "list-check" | "list-checks" | "list-chevrons-down-up" | "list-chevrons-up-down" | "list-clock" | "list-collapse" | "list-end" | "list-filter" | "list-filter-plus" | "list-indent-decrease" | "list-indent-increase" | "list-minus" | "list-music" | "list-ordered" | "list-plus" | "list-restart" | "list-sort-ascending" | "list-sort-descending" | "list-start" | "list-todo" | "list-tree" | "list-video" | "list-x" | "loader" | "loader-2" | "loader-circle" | "loader-pinwheel" | "locate" | "locate-fixed" | "locate-off" | "location-edit" | "lock" | "lock-keyhole" | "lock-keyhole-open" | "lock-open" | "log-in" | "log-out" | "logs" | "lollipop" | "luggage" | "m-square" | "magnet" | "mail" | "mail-badge" | "mail-check" | "mail-minus" | "mail-open" | "mail-plus" | "mail-question" | "mail-question-mark" | "mail-search" | "mail-warning" | "mail-x" | "mailbox" | "mails" | "map" | "map-minus" | "map-pin" | "map-pin-check" | "map-pin-check-inside" | "map-pin-house" | "map-pin-minus" | "map-pin-minus-inside" | "map-pin-off" | "map-pin-pen" | "map-pin-plus" | "map-pin-plus-inside" | "map-pin-search" | "map-pin-x" | "map-pin-x-inside" | "map-pinned" | "map-plus" | "mars" | "mars-stroke" | "martini" | "maximize" | "maximize-2" | "medal" | "megaphone" | "megaphone-off" | "meh" | "memory-stick" | "menu" | "menu-square" | "merge" | "message-circle" | "message-circle-check" | "message-circle-code" | "message-circle-dashed" | "message-circle-heart" | "message-circle-more" | "message-circle-off" | "message-circle-plus" | "message-circle-question" | "message-circle-question-mark" | "message-circle-reply" | "message-circle-warning" | "message-circle-x" | "message-square" | "message-square-check" | "message-square-code" | "message-square-dashed" | "message-square-diff" | "message-square-dot" | "message-square-heart" | "message-square-lock" | "message-square-more" | "message-square-off" | "message-square-plus" | "message-square-quote" | "message-square-reply" | "message-square-share" | "message-square-text" | "message-square-warning" | "message-square-x" | "messages-square" | "metronome" | "mic" | "mic-2" | "mic-audio-lines" | "mic-off" | "mic-signal" | "mic-vocal" | "microchip" | "microscope" | "microwave" | "midi-port" | "milestone" | "milk" | "milk-off" | "minimize" | "minimize-2" | "minus" | "minus-circle" | "minus-square" | "mirror-rectangular" | "mirror-round" | "monitor" | "monitor-check" | "monitor-cloud" | "monitor-cog" | "monitor-dot" | "monitor-down" | "monitor-off" | "monitor-pause" | "monitor-play" | "monitor-smartphone" | "monitor-speaker" | "monitor-stop" | "monitor-up" | "monitor-x" | "moon" | "moon-star" | "mop" | "mop-sparkles" | "more-horizontal" | "more-vertical" | "mosque" | "motorbike" | "mountain" | "mountain-snow" | "mouse" | "mouse-left" | "mouse-off" | "mouse-pointer" | "mouse-pointer-2" | "mouse-pointer-2-off" | "mouse-pointer-ban" | "mouse-pointer-click" | "mouse-pointer-square-dashed" | "mouse-right" | "move" | "move-3-d" | "move-3d" | "move-diagonal" | "move-diagonal-2" | "move-down" | "move-down-left" | "move-down-right" | "move-horizontal" | "move-left" | "move-right" | "move-up" | "move-up-left" | "move-up-right" | "move-vertical" | "music" | "music-2" | "music-3" | "music-4" | "navigation" | "navigation-2" | "navigation-2-off" | "navigation-off" | "network" | "newspaper" | "nfc" | "non-binary" | "notebook" | "notebook-pen" | "notebook-tabs" | "notebook-text" | "notepad-text" | "notepad-text-dashed" | "nut" | "nut-off" | "octagon" | "octagon-alert" | "octagon-minus" | "octagon-pause" | "octagon-x" | "omega" | "option" | "orbit" | "origami" | "outdent" | "package" | "package-2" | "package-check" | "package-minus" | "package-open" | "package-plus" | "package-search" | "package-x" | "paint-bucket" | "paint-roller" | "paintbrush" | "paintbrush-2" | "paintbrush-vertical" | "palette" | "palmtree" | "panda" | "panel-bottom" | "panel-bottom-close" | "panel-bottom-dashed" | "panel-bottom-inactive" | "panel-bottom-open" | "panel-left" | "panel-left-close" | "panel-left-dashed" | "panel-left-inactive" | "panel-left-open" | "panel-left-right-dashed" | "panel-right" | "panel-right-close" | "panel-right-dashed" | "panel-right-inactive" | "panel-right-open" | "panel-top" | "panel-top-bottom-dashed" | "panel-top-close" | "panel-top-dashed" | "panel-top-inactive" | "panel-top-open" | "panels-left-bottom" | "panels-left-right" | "panels-right-bottom" | "panels-top-bottom" | "panels-top-left" | "paper-bag" | "paperclip" | "parasol" | "parentheses" | "parking-circle" | "parking-circle-off" | "parking-meter" | "parking-square" | "parking-square-off" | "party-popper" | "pause" | "pause-circle" | "pause-octagon" | "paw-print" | "pc-case" | "pen" | "pen-box" | "pen-line" | "pen-off" | "pen-square" | "pen-tool" | "pencil" | "pencil-line" | "pencil-off" | "pencil-ruler" | "pencil-sparkles" | "pentagon" | "percent" | "percent-circle" | "percent-diamond" | "percent-square" | "person-standing" | "phi" | "philippine-peso" | "phone" | "phone-call" | "phone-forwarded" | "phone-incoming" | "phone-missed" | "phone-off" | "phone-outgoing" | "pi" | "pi-square" | "piano" | "pickaxe" | "picture-in-picture" | "picture-in-picture-2" | "pie-chart" | "piggy-bank" | "pilcrow" | "pilcrow-left" | "pilcrow-right" | "pilcrow-square" | "pill" | "pill-bottle" | "pin" | "pin-off" | "pipette" | "pizza" | "plane" | "plane-landing" | "plane-takeoff" | "play" | "play-circle" | "play-off" | "play-square" | "plug" | "plug-2" | "plug-zap" | "plug-zap-2" | "plus" | "plus-circle" | "plus-square" | "pocket-knife" | "podcast" | "podium" | "pointer" | "pointer-off" | "popcorn" | "popsicle" | "pound-sterling" | "power" | "power-circle" | "power-off" | "power-square" | "presentation" | "printer" | "printer-check" | "printer-x" | "projector" | "proportions" | "puzzle" | "pyramid" | "qr-code" | "quote" | "rabbit" | "radar" | "radiation" | "radical" | "radio" | "radio-off" | "radio-receiver" | "radio-tower" | "radius" | "rainbow" | "rat" | "ratio" | "receipt" | "receipt-cent" | "receipt-euro" | "receipt-indian-rupee" | "receipt-japanese-yen" | "receipt-pound-sterling" | "receipt-russian-ruble" | "receipt-swiss-franc" | "receipt-text" | "receipt-turkish-lira" | "rectangle-circle" | "rectangle-ellipsis" | "rectangle-goggles" | "rectangle-horizontal" | "rectangle-vertical" | "recycle" | "redo" | "redo-2" | "redo-dot" | "refresh-ccw" | "refresh-ccw-dot" | "refresh-cw" | "refresh-cw-off" | "refrigerator" | "regex" | "remove-formatting" | "repeat" | "repeat-1" | "repeat-2" | "repeat-off" | "replace" | "replace-all" | "reply" | "reply-all" | "rewind" | "ribbon" | "road" | "rocket" | "rocking-chair" | "roller-coaster" | "rose" | "rotate-3-d" | "rotate-3d" | "rotate-ccw" | "rotate-ccw-clock" | "rotate-ccw-key" | "rotate-ccw-square" | "rotate-cw" | "rotate-cw-fading-clock" | "rotate-cw-square" | "route" | "route-off" | "router" | "rows" | "rows-2" | "rows-3" | "rows-4" | "rss" | "ruler" | "ruler-dimension-line" | "russian-ruble" | "sailboat" | "salad" | "sandwich" | "satellite" | "satellite-dish" | "saudi-riyal" | "save" | "save-all" | "save-check" | "save-off" | "save-pen" | "save-plus" | "scale" | "scale-3-d" | "scale-3d" | "scaling" | "scan" | "scan-barcode" | "scan-box" | "scan-eye" | "scan-face" | "scan-heart" | "scan-line" | "scan-qr-code" | "scan-search" | "scan-square" | "scan-text" | "scatter-chart" | "school" | "school-2" | "scissors" | "scissors-line-dashed" | "scissors-square" | "scissors-square-dashed-bottom" | "scooter" | "screen-share" | "screen-share-off" | "scroll" | "scroll-text" | "search" | "search-alert" | "search-check" | "search-code" | "search-slash" | "search-x" | "section" | "send" | "send-horizonal" | "send-horizontal" | "send-to-back" | "separator-horizontal" | "separator-vertical" | "server" | "server-cog" | "server-crash" | "server-off" | "server-plus" | "settings" | "settings-2" | "shapes" | "share" | "share-2" | "sheet" | "shell" | "shelving-unit" | "shield" | "shield-alert" | "shield-ban" | "shield-check" | "shield-close" | "shield-cog" | "shield-cog-corner" | "shield-ellipsis" | "shield-half" | "shield-keyhole" | "shield-lock" | "shield-minus" | "shield-off" | "shield-plus" | "shield-question" | "shield-question-mark" | "shield-user" | "shield-x" | "ship" | "ship-wheel" | "shirt" | "shopping-bag" | "shopping-basket" | "shopping-cart" | "shovel" | "shower-head" | "shredder" | "shrimp" | "shrink" | "shrub" | "shuffle" | "sidebar" | "sidebar-close" | "sidebar-open" | "sigma" | "sigma-square" | "signal" | "signal-high" | "signal-low" | "signal-medium" | "signal-zero" | "signature" | "signpost" | "signpost-big" | "siren" | "skip-back" | "skip-forward" | "skull" | "slash" | "slash-square" | "slice" | "sliders" | "sliders-horizontal" | "sliders-vertical" | "smartphone" | "smartphone-charging" | "smartphone-nfc" | "smile" | "smile-plus" | "snail" | "snowflake" | "soap-dispenser-droplet" | "sofa" | "solar-panel" | "sort-asc" | "sort-desc" | "soup" | "space" | "spade" | "sparkle" | "sparkles" | "speaker" | "speech" | "spell-check" | "spell-check-2" | "spline" | "spline-pointer" | "split" | "split-square-horizontal" | "split-square-vertical" | "spool" | "sport-shoe" | "spotlight" | "spray-can" | "sprout" | "square" | "square-activity" | "square-arrow-down" | "square-arrow-down-left" | "square-arrow-down-right" | "square-arrow-left" | "square-arrow-out-down-left" | "square-arrow-out-down-right" | "square-arrow-out-up-left" | "square-arrow-out-up-right" | "square-arrow-right" | "square-arrow-right-enter" | "square-arrow-right-exit" | "square-arrow-up" | "square-arrow-up-left" | "square-arrow-up-right" | "square-asterisk" | "square-bottom-dashed-scissors" | "square-centerline-dashed-horizontal" | "square-centerline-dashed-vertical" | "square-chart-gantt" | "square-check" | "square-check-big" | "square-chevron-down" | "square-chevron-left" | "square-chevron-right" | "square-chevron-up" | "square-code" | "square-dashed" | "square-dashed-bottom" | "square-dashed-bottom-code" | "square-dashed-kanban" | "square-dashed-mouse-pointer" | "square-dashed-text" | "square-dashed-top-solid" | "square-dimensions" | "square-divide" | "square-dot" | "square-equal" | "square-function" | "square-gantt-chart" | "square-kanban" | "square-library" | "square-m" | "square-menu" | "square-minus" | "square-mouse-pointer" | "square-off" | "square-parking" | "square-parking-off" | "square-pause" | "square-pen" | "square-percent" | "square-pi" | "square-pilcrow" | "square-play" | "square-plus" | "square-power" | "square-radical" | "square-round-corner" | "square-scissors" | "square-sigma" | "square-slash" | "square-split-horizontal" | "square-split-vertical" | "square-square" | "square-stack" | "square-star" | "square-stop" | "square-terminal" | "square-text" | "square-user" | "square-user-round" | "square-x" | "squares-exclude" | "squares-intersect" | "squares-subtract" | "squares-unite" | "squircle" | "squircle-dashed" | "squirrel" | "stamp" | "star" | "star-check" | "star-half" | "star-minus" | "star-off" | "star-plus" | "star-x" | "stars" | "step-back" | "step-forward" | "stethoscope" | "sticker" | "sticky-note" | "sticky-note-check" | "sticky-note-minus" | "sticky-note-off" | "sticky-note-plus" | "sticky-note-x" | "sticky-notes" | "stone" | "stop-circle" | "store" | "stretch-horizontal" | "stretch-vertical" | "strikethrough" | "subscript" | "subtitles" | "summary" | "sun" | "sun-dim" | "sun-medium" | "sun-moon" | "sun-snow" | "sunrise" | "sunset" | "superscript" | "swatch-book" | "swiss-franc" | "switch-camera" | "sword" | "swords" | "syringe" | "table" | "table-2" | "table-cells-merge" | "table-cells-split" | "table-columns-split" | "table-config" | "table-of-contents" | "table-properties" | "table-rows-split" | "tablet" | "tablet-smartphone" | "tablets" | "tag" | "tag-plus" | "tag-x" | "tags" | "tally-1" | "tally-2" | "tally-3" | "tally-4" | "tally-5" | "tangent" | "target" | "telescope" | "tent" | "tent-tree" | "terminal" | "terminal-square" | "test-tube" | "test-tube-2" | "test-tube-diagonal" | "test-tubes" | "text" | "text-align-center" | "text-align-end" | "text-align-justify" | "text-align-start" | "text-cursor" | "text-cursor-input" | "text-initial" | "text-quote" | "text-search" | "text-select" | "text-selection" | "text-wrap" | "theater" | "thermometer" | "thermometer-snowflake" | "thermometer-sun" | "thumbs-down" | "thumbs-up" | "ticket" | "ticket-check" | "ticket-minus" | "ticket-percent" | "ticket-plus" | "ticket-slash" | "ticket-x" | "tickets" | "tickets-plane" | "timeline" | "timer" | "timer-off" | "timer-reset" | "toggle-left" | "toggle-right" | "toilet" | "tool-case" | "toolbox" | "tornado" | "torus" | "touchpad" | "touchpad-off" | "towel-rack" | "tower-control" | "toy-brick" | "tractor" | "traffic-cone" | "train" | "train-front" | "train-front-tunnel" | "train-track" | "tram-front" | "transgender" | "trash" | "trash-2" | "tree-deciduous" | "tree-palm" | "tree-pine" | "trees" | "trending-down" | "trending-up" | "trending-up-down" | "triangle" | "triangle-alert" | "triangle-dashed" | "triangle-right" | "trophy" | "truck" | "truck-electric" | "turkish-lira" | "turntable" | "turtle" | "tv" | "tv-2" | "tv-minimal" | "tv-minimal-play" | "type" | "type-outline" | "umbrella" | "umbrella-off" | "underline" | "undo" | "undo-2" | "undo-dot" | "unfold-horizontal" | "unfold-vertical" | "ungroup" | "university" | "unlink" | "unlink-2" | "unlock" | "unlock-keyhole" | "unplug" | "upload" | "upload-cloud" | "usb" | "usb-c-port" | "user" | "user-2" | "user-check" | "user-check-2" | "user-circle" | "user-circle-2" | "user-cog" | "user-cog-2" | "user-key" | "user-lock" | "user-minus" | "user-minus-2" | "user-pen" | "user-plus" | "user-plus-2" | "user-round" | "user-round-arrow-left" | "user-round-check" | "user-round-cog" | "user-round-key" | "user-round-minus" | "user-round-pen" | "user-round-plus" | "user-round-search" | "user-round-x" | "user-search" | "user-shield" | "user-square" | "user-square-2" | "user-star" | "user-x" | "user-x-2" | "users" | "users-2" | "users-round" | "utensils" | "utensils-crossed" | "utility-pole" | "van" | "variable" | "vault" | "vector-square" | "vegan" | "venetian-mask" | "venus" | "venus-and-mars" | "verified" | "vibrate" | "vibrate-off" | "video" | "video-off" | "videotape" | "view" | "voicemail" | "volleyball" | "volume" | "volume-1" | "volume-2" | "volume-off" | "volume-x" | "vote" | "wallet" | "wallet-2" | "wallet-cards" | "wallet-minimal" | "wallpaper" | "wand" | "wand-2" | "wand-sparkles" | "warehouse" | "washing-machine" | "watch" | "waves" | "waves-arrow-down" | "waves-arrow-up" | "waves-horizontal" | "waves-ladder" | "waves-vertical" | "waypoints" | "webcam" | "webcam-off" | "webhook" | "webhook-off" | "weight" | "weight-tilde" | "wheat" | "wheat-off" | "whole-word" | "wifi" | "wifi-cog" | "wifi-high" | "wifi-low" | "wifi-off" | "wifi-pen" | "wifi-sync" | "wifi-zero" | "wind" | "wind-arrow-down" | "wine" | "wine-off" | "workflow" | "worm" | "wrap-text" | "wrench" | "wrench-off" | "x" | "x-circle" | "x-line-top" | "x-octagon" | "x-square" | "zap" | "zap-off" | "zodiac-aquarius" | "zodiac-aries" | "zodiac-cancer" | "zodiac-capricorn" | "zodiac-gemini" | "zodiac-leo" | "zodiac-libra" | "zodiac-ophiuchus" | "zodiac-pisces" | "zodiac-sagittarius" | "zodiac-scorpio" | "zodiac-taurus" | "zodiac-virgo" | "zoom-in" | "zoom-out";
9
9
  export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
10
10
  export interface BannerAction { label: string; variant: "primary" | "secondary"; onTrigger: () => void | Promise<void>; }
11
- export interface ConversationProps {
12
- agentId: string;
13
- scenario: string;
14
- context: string;
11
+ export type ConversationProps = {
15
12
  title?: string;
16
13
  placeholder?: string;
17
14
  onConversationCreated?: (event: import("./conversation.js").ConversationCreatedEvent) => void | Promise<void>;
@@ -20,12 +17,9 @@ export interface ConversationProps {
20
17
  onToolCall?: (event: import("./conversation.js").ToolCallEvent) => void | Promise<void>;
21
18
  onError?: (event: import("./conversation.js").ConversationErrorEvent) => void | Promise<void>;
22
19
  style?: AppStyle;
23
- }
20
+ } & import("./conversation.js").ConversationRootBinding;
24
21
  export const Conversation: ComponentType<ConversationProps> & { Root: typeof ConversationRoot; Messages: typeof ConversationMessages; Composer: typeof ConversationComposer; History: typeof ConversationHistory; NewButton: typeof ConversationNewButton; StopButton: typeof ConversationStopButton; Status: typeof ConversationStatus };
25
- export interface ConversationRootProps {
26
- agentId: string;
27
- scenario: string;
28
- context: string;
22
+ export type ConversationRootProps = {
29
23
  title?: string;
30
24
  placeholder?: string;
31
25
  onConversationCreated?: (event: import("./conversation.js").ConversationCreatedEvent) => void | Promise<void>;
@@ -35,12 +29,13 @@ export interface ConversationRootProps {
35
29
  onError?: (event: import("./conversation.js").ConversationErrorEvent) => void | Promise<void>;
36
30
  style?: AppStyle;
37
31
  children?: ReactNode;
38
- }
32
+ } & import("./conversation.js").ConversationRootBinding;
39
33
  export const ConversationRoot: ComponentType<ConversationRootProps>;
40
34
  export interface ConversationMessagesProps {
41
35
  fill?: boolean;
42
36
  emptyTitle?: string;
43
37
  emptyDescription?: string;
38
+ toolRenderers?: Readonly<Record<string, ComponentType<import("./conversation.js").ToolResultView>>>;
44
39
  }
45
40
  export const ConversationMessages: ComponentType<ConversationMessagesProps>;
46
41
  export interface ConversationComposerProps {
@@ -400,6 +395,8 @@ export interface WidgetDecorationProps {
400
395
  children: ReactNode;
401
396
  }
402
397
  export const WidgetDecoration: ComponentType<WidgetDecorationProps>;
398
+ export { useConversation } from "./conversation.js";
399
+ export type { ConversationController, ConversationOptions, ToolResultView, ConversationReference, PreparedConversationReference } from "./conversation.js";
403
400
  export type { ConversationCreatedEvent, TurnStartedEvent, TurnFinishedEvent, ToolCallEvent, ConversationErrorEvent } from "./conversation.js";
404
401
  export const Typography: { Title: typeof TypographyTitle; Body: typeof TypographyBody; Caption: typeof TypographyCaption };
405
402
  export const Widget: { TextWidget: typeof WidgetTextWidget; Title: typeof WidgetTitle; Text: { Primary: typeof WidgetTextPrimary; Secondary: typeof WidgetTextSecondary }; Loading: typeof WidgetLoading; Decoration: typeof WidgetDecoration };
package/src/ui/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // 此文件由 scripts/generate-components.mjs 生成,请修改产品能力声明。
2
2
  import * as React from "react";
3
3
  import { createUiComponents } from "./factory.js";
4
- export const { Conversation, ConversationRoot, ConversationMessages, ConversationComposer, ConversationHistory, ConversationNewButton, ConversationStopButton, ConversationStatus, NativeDOM, Icon, Stack, SplitLayout, SplitLayoutMain, SplitLayoutAside, SplitLayoutActions, Section, Banner, Badge, LoadingState, Skeleton, Card, Grid, EmptyState, ZhikeLink, Button, SearchInput, FilterSelect, Switch, LookupCell, Tabs, TabsList, TabsTrigger, TabsContent, Table, TableHeader, TableBody, TableRow, TableCell, Avatar, DescriptionList, DescriptionListItem, DescriptionListMenu, DescriptionListMenuItem, Divider, Chart, DashboardSurface, DashboardLayout, DashboardPanel, MetricReadout, AnalysisView, Json, Link, StatusBadge, TextBlock, TypographyTitle, TypographyBody, TypographyCaption, TableHeaderCell, WidgetTextWidget, WidgetTitle, WidgetTextPrimary, WidgetTextSecondary, WidgetLoading, WidgetDecoration, Typography, Widget } = createUiComponents(React);
4
+ export const { useConversation, Conversation, ConversationRoot, ConversationMessages, ConversationComposer, ConversationHistory, ConversationNewButton, ConversationStopButton, ConversationStatus, NativeDOM, Icon, Stack, SplitLayout, SplitLayoutMain, SplitLayoutAside, SplitLayoutActions, Section, Banner, Badge, LoadingState, Skeleton, Card, Grid, EmptyState, ZhikeLink, Button, SearchInput, FilterSelect, Switch, LookupCell, Tabs, TabsList, TabsTrigger, TabsContent, Table, TableHeader, TableBody, TableRow, TableCell, Avatar, DescriptionList, DescriptionListItem, DescriptionListMenu, DescriptionListMenuItem, Divider, Chart, DashboardSurface, DashboardLayout, DashboardPanel, MetricReadout, AnalysisView, Json, Link, StatusBadge, TextBlock, TypographyTitle, TypographyBody, TypographyCaption, TableHeaderCell, WidgetTextWidget, WidgetTitle, WidgetTextPrimary, WidgetTextSecondary, WidgetLoading, WidgetDecoration, Typography, Widget } = createUiComponents(React);