@dingyi-tech/chat-renderer-sdk 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,6 +27,7 @@ dingyi-agno component pack .
27
27
  ```
28
28
 
29
29
  `pack` 会自动安装前端依赖、类型检查、构建单文件 ESM 并生成 ZIP,不需要手工维护平台入口。
30
+ SDK 会按锁文件选择对应的包管理器,并在依赖文件未变化时跳过重复安装。
30
31
 
31
32
  生成的 Renderer 工程保持为普通前端目录,不包含隐藏适配代码:
32
33
 
@@ -73,6 +74,9 @@ dingyi-renderer build --framework react
73
74
  生产构建固定输出 `dist/renderer.mjs`。自定义源码路径等高级场景可以使用
74
75
  `@dingyi-tech/chat-renderer-sdk/vite`,普通组件工程不应自行维护构建入口。
75
76
 
77
+ Host 会注入 `presentation.mode`:中台聊天默认是 `user`,本地开发是 `debug`。Renderer
78
+ 可以据此折叠技术细节,但应始终保留完整 payload。
79
+
76
80
  ## React
77
81
 
78
82
  ```tsx
@@ -85,7 +89,8 @@ type SalesData = {
85
89
  total: number;
86
90
  };
87
91
 
88
- export default function Renderer({ data, action, readonly }: RendererProps<SalesData>) {
92
+ export default function Renderer({ data, action, readonly, presentation }: RendererProps<SalesData>) {
93
+ const isDebug = presentation.mode === "debug";
89
94
  const [busy, setBusy] = useState(false);
90
95
 
91
96
  const refresh = async () => {
@@ -157,6 +162,8 @@ export default render;
157
162
 
158
163
  常规筛选、分页和提交只使用 `action()`。只有启动新对话、恢复 HITL 或调用宿主系统时才需要 `runtime`。
159
164
 
165
+ 自定义 Renderer 只在组件显式输出 `ctx.ui.render()` 时启用。普通字符串或 Markdown 回复继续使用平台默认 Renderer,不要求业务组件处理空 Payload。
166
+
160
167
  ## Python 配套代码
161
168
 
162
169
  ```python
@@ -210,22 +217,111 @@ pnpm add @dingyi-tech/chat-renderer-sdk
210
217
 
211
218
  ## 发布到 npm
212
219
 
213
- `dingyi-tech` 是 npm 账号 `dingyi-tech` 自带的个人 scope,不需要额外创建 Organization。发布者需要登录该账号,并为 npm 写操作启用 2FA;自动化发布时则使用具有该 scope 发布权限的 Granular Access Token。令牌只放在本机或 CI 环境,不写入仓库:
220
+ SDK 包名是 `@dingyi-tech/chat-renderer-sdk`,发布地址固定为 npm 官方 Registry:
221
+ `https://registry.npmjs.org/`。发布前必须递增一个尚未发布的版本号,不能覆盖已经存在的版本。
222
+
223
+ ### 方式一:在仓库根目录发布
224
+
225
+ 适合维护本项目的开发者。以下命令会依次完成类型检查、构建、发布文件检查和 npm 发布:
214
226
 
215
227
  ```bash
216
- export NPM_TOKEN="npm_xxx"
217
- npm config set '//registry.npmjs.org/:_authToken' "$NPM_TOKEN" --location=user
218
- npm whoami --registry https://registry.npmjs.org/
228
+ # 1. 检查 SDK
229
+ npm run check:renderer-sdk
230
+
231
+ # 2. 递增版本号(按实际变更选择 patch、minor 或 major)
232
+ pnpm --dir sdk/chat-renderer version patch --no-git-tag-version
233
+
234
+ # 3. 预览将要进入 tarball 的文件
235
+ pnpm --dir sdk/chat-renderer pack --dry-run
236
+
237
+ # 4. 发布公开包
238
+ npm run publish:renderer-sdk
219
239
  ```
220
240
 
221
- 先检查实际制品,再发布:
241
+ 如果只想生成发布包、不发布到 npm:
222
242
 
223
243
  ```bash
224
244
  npm run pack:renderer-sdk
225
- npm run publish:renderer-sdk
226
245
  ```
227
246
 
228
- `prepack` 会重新构建 `dist` 并检查所有公开入口,`publishConfig` 固定发布到 npm 官方 Registry 且按 public package 发布。正式发版前使用 `pnpm --dir sdk/chat-renderer version patch|minor|major` 更新版本;启用 npm 2FA 的账号还需要按提示输入 OTP。
247
+ ### 方式二:在 SDK 目录直接发布
248
+
249
+ 适合只拿 SDK 子目录单独发版:
250
+
251
+ ```bash
252
+ cd sdk/chat-renderer
253
+
254
+ # 1. 修改版本号
255
+ npm version patch --no-git-tag-version
256
+
257
+ # 2. 类型检查、构建并检查发布产物
258
+ npm run check
259
+
260
+ # 3. 预览发布文件
261
+ npm pack --dry-run
262
+
263
+ # 4. 发布到 npm
264
+ npm publish --access public --registry https://registry.npmjs.org/
265
+ ```
266
+
267
+ `npm publish` 会触发 `prepack`,重新构建 `dist` 并检查公开入口。`package.json` 的
268
+ `files` 白名单只允许发布 `dist/**/*.js`、`dist/**/*.d.ts` 和 `README.md`。
269
+
270
+ ### 发布前认证
271
+
272
+ 先确认当前登录账号和 Registry:
273
+
274
+ ```bash
275
+ npm whoami --registry https://registry.npmjs.org/
276
+ ```
277
+
278
+ 未登录时,在交互式终端执行:
279
+
280
+ ```bash
281
+ npm login --auth-type=web --registry https://registry.npmjs.org/
282
+ ```
283
+
284
+ 当前 npm 账号使用 WebAuthn / Passkey 认证,不是项目生成的验证码。命令可能提示:
285
+
286
+ ```text
287
+ Press ENTER to open in the browser...
288
+ ```
289
+
290
+ 按 Enter 后,在浏览器中使用 Touch ID、指纹或安全密钥确认。发布命令也可能打开相同的浏览器认证流程,因此需要在真实的交互式终端执行;不要把账号密码、Passkey 或 Token 写入仓库。
291
+
292
+ ### CI 自动发布
293
+
294
+ CI 不适合使用浏览器指纹认证时,使用具有该 scope 发布权限的 npm Granular Access Token,并通过 CI Secret 注入 `NODE_AUTH_TOKEN`:
295
+
296
+ ```bash
297
+ export NODE_AUTH_TOKEN="npm_xxx"
298
+ cat > ~/.npmrc <<'EOF'
299
+ //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
300
+ EOF
301
+
302
+ cd sdk/chat-renderer
303
+ npm run check
304
+ npm pack --dry-run
305
+ npm publish --access public --registry https://registry.npmjs.org/
306
+ ```
307
+
308
+ Token 只放在本机密钥存储或 CI Secret 中,不要提交 `.npmrc` 或把 Token 写入脚本。
309
+
310
+ ### 发布后验证
311
+
312
+ ```bash
313
+ npm view @dingyi-tech/chat-renderer-sdk versions --json \
314
+ --registry https://registry.npmjs.org/
315
+
316
+ npm view @dingyi-tech/chat-renderer-sdk dist-tags --json \
317
+ --registry https://registry.npmjs.org/
318
+
319
+ # 在临时目录验证外部安装
320
+ npm install @dingyi-tech/chat-renderer-sdk@latest
321
+ ```
322
+
323
+ ### sourcemap 检查
229
324
 
230
- 发布包禁止包含 sourcemap。构建会先清空 `dist`,并关闭 JavaScript 与声明文件的
231
- sourcemap;`prepack` 发现 `.map` 文件或 `sourceMappingURL` 时会直接终止发布。
325
+ 发布包禁止包含 sourcemap。构建前会清空 `dist`,并关闭 JavaScript 与声明文件的
326
+ sourcemap;`prepack` 发现 `.map` 文件或 `sourceMappingURL` 时会直接终止发布。执行
327
+ `npm pack --dry-run` 时,清单中不应出现 `.map` 或 `sourceMappingURL`。
package/dist/cli.js CHANGED
@@ -6,13 +6,14 @@ import { pathToFileURL } from "node:url";
6
6
  import { build as viteBuild, createServer, } from "vite";
7
7
  import { createRendererViteConfig, resolveRendererSourcePaths, } from "./vite.js";
8
8
  const usage = `Usage:
9
- dingyi-renderer build --framework <react|vue|native>
9
+ dingyi-renderer build --framework <react|vue|native> [--watch]
10
10
  dingyi-renderer dev --framework <react|vue|native> [--host 127.0.0.1] [--port 4174]
11
11
 
12
12
  Options:
13
13
  --root <path> Renderer UI root (default: current directory)
14
14
  --host <host> Development server host
15
15
  --port <port> Development server port
16
+ --watch Rebuild the Renderer artifact when sources change
16
17
  -h, --help Show this help
17
18
  `;
18
19
  function optionValue(args, index, inline) {
@@ -38,6 +39,7 @@ function parseArguments(args) {
38
39
  let root = process.cwd();
39
40
  let host = "127.0.0.1";
40
41
  let port = 4174;
42
+ let watch = false;
41
43
  for (let index = 1; index < args.length; index += 1) {
42
44
  const argument = args[index];
43
45
  const separator = argument.indexOf("=");
@@ -62,13 +64,18 @@ function parseArguments(args) {
62
64
  throw new Error(`Invalid port: ${String(port)}`);
63
65
  }
64
66
  }
67
+ else if (flag === "--watch") {
68
+ watch = true;
69
+ }
65
70
  else {
66
71
  throw new Error(`Unknown option: ${argument}`);
67
72
  }
68
73
  }
69
74
  if (!framework)
70
75
  throw new Error("--framework is required");
71
- return { command, framework, root, host, port };
76
+ if (watch && command !== "build")
77
+ throw new Error("--watch is only supported by the build command");
78
+ return { command, framework, root, host, port, watch };
72
79
  }
73
80
  async function assertRendererSources(options) {
74
81
  const sources = resolveRendererSourcePaths(options);
@@ -103,11 +110,12 @@ async function run(options) {
103
110
  const plugins = await loadFrameworkPlugins(options);
104
111
  const config = createRendererViteConfig({
105
112
  framework: options.framework,
113
+ mode: options.command === "dev" ? "development" : "production",
106
114
  root: options.root,
107
115
  plugins,
108
116
  });
109
117
  if (options.command === "build") {
110
- await viteBuild(config);
118
+ await viteBuild(options.watch ? { ...config, build: { ...config.build, watch: {} } } : config);
111
119
  return;
112
120
  }
113
121
  const server = await createServer({
package/dist/index.d.ts CHANGED
@@ -1,25 +1,42 @@
1
1
  export declare const RENDERER_PROTOCOL: "dingyi.renderer.v1";
2
- export type JsonValue = string | number | boolean | null | JsonValue[] | {
2
+ export type JsonValue = string | number | boolean | null | JsonValue[] | JsonObject;
3
+ export type JsonObject = {
3
4
  [key: string]: JsonValue;
4
5
  };
5
- export type RendererPayload = Record<string, unknown>;
6
+ export type RendererPayload = JsonObject;
7
+ export type RendererPresentation = {
8
+ mode: "user" | "debug";
9
+ };
10
+ export declare function isJsonValue(value: unknown): value is JsonValue;
11
+ export declare function isJsonObject(value: unknown): value is JsonObject;
12
+ export type RendererSnapshot = {
13
+ rendererKey?: string;
14
+ rendererVersion?: string;
15
+ rendererDigest?: string;
16
+ messageId?: string;
17
+ revision?: number;
18
+ payload?: RendererPayload;
19
+ fallbackMarkdown?: string;
20
+ };
6
21
  export type RendererActionResult = {
7
- snapshot?: {
8
- rendererKey?: string;
9
- rendererVersion?: string;
10
- rendererDigest?: string;
11
- messageId?: string;
12
- revision?: number;
13
- payload?: RendererPayload;
14
- fallbackMarkdown?: string;
15
- };
16
- [key: string]: unknown;
22
+ snapshot?: RendererSnapshot;
23
+ [key: string]: JsonValue | RendererSnapshot | undefined;
17
24
  };
18
25
  export type RendererBridge = {
26
+ /** Read session-owned media. Available in history; never executes a renderer action. */
27
+ loadMedia?(reference: {
28
+ storage_key: string;
29
+ session_id?: string;
30
+ mime_type?: string;
31
+ filename?: string;
32
+ }, options?: {
33
+ thumbnail?: boolean;
34
+ signal?: AbortSignal;
35
+ }): Promise<Blob>;
19
36
  invoke(action: string, input: RendererPayload): Promise<RendererActionResult>;
20
37
  reply(content: string, metadata?: RendererPayload): Promise<void>;
21
38
  resume(input: RendererPayload): Promise<void>;
22
- host(action: string, input: RendererPayload): Promise<unknown>;
39
+ host(action: string, input: RendererPayload): Promise<JsonValue>;
23
40
  };
24
41
  export type RendererContext = {
25
42
  protocol: typeof RENDERER_PROTOCOL;
@@ -40,6 +57,7 @@ export type RendererContext = {
40
57
  locale: "zh-CN" | "en-US";
41
58
  readonly: boolean;
42
59
  theme: Record<string, string>;
60
+ presentation: RendererPresentation;
43
61
  bridge: RendererBridge;
44
62
  };
45
63
  export type RendererProps<T extends object = RendererPayload> = {
@@ -50,6 +68,7 @@ export type RendererProps<T extends object = RendererPayload> = {
50
68
  locale: RendererContext["locale"];
51
69
  readonly: boolean;
52
70
  theme: RendererContext["theme"];
71
+ presentation: RendererPresentation;
53
72
  action(action: string, input?: RendererPayload): Promise<RendererActionResult>;
54
73
  /** Advanced lifecycle operations. Most renderers only need `action()`. */
55
74
  runtime: RendererBridge;
@@ -75,6 +94,7 @@ export declare function createRendererPreviewProps<T extends object = RendererPa
75
94
  content?: string;
76
95
  locale?: RendererContext["locale"];
77
96
  readonly?: boolean;
97
+ presentation?: RendererPresentation;
78
98
  onAction?: (action: string, input: RendererPayload) => Promise<RendererActionResult> | RendererActionResult;
79
99
  }): RendererProps<T>;
80
100
  export declare const rendererThemeVariables: Readonly<{
@@ -100,8 +120,11 @@ export type RendererActionState = {
100
120
  result?: RendererActionResult;
101
121
  error?: Error;
102
122
  };
103
- export declare function createRendererActionState(onChange?: (state: Readonly<RendererActionState>) => void): {
123
+ export type RendererActionStates = Readonly<Record<string, Readonly<RendererActionState>>>;
124
+ export declare function createRendererActionState(onChange?: (latest: Readonly<RendererActionState>, states: RendererActionStates) => void): {
104
125
  readonly value: Readonly<RendererActionState>;
126
+ readonly values: RendererActionStates;
127
+ get(action: string): Readonly<RendererActionState>;
105
128
  invoke(bridge: RendererBridge, action: string, input: RendererPayload): Promise<RendererActionResult>;
106
- reset(): void;
129
+ reset(action?: string): void;
107
130
  };
package/dist/index.js CHANGED
@@ -1,4 +1,40 @@
1
1
  export const RENDERER_PROTOCOL = "dingyi.renderer.v1";
2
+ export function isJsonValue(value) {
3
+ return validateJsonValue(value, new Set(), 0);
4
+ }
5
+ export function isJsonObject(value) {
6
+ return Boolean(value) && !Array.isArray(value) && isJsonValue(value);
7
+ }
8
+ function validateJsonValue(value, active, depth) {
9
+ if (depth > 100)
10
+ return false;
11
+ if (value === null || typeof value === "string" || typeof value === "boolean")
12
+ return true;
13
+ if (typeof value === "number")
14
+ return Number.isFinite(value);
15
+ if (typeof value !== "object")
16
+ return false;
17
+ if (active.has(value))
18
+ return false;
19
+ active.add(value);
20
+ try {
21
+ if (Array.isArray(value)) {
22
+ return value.every((item) => validateJsonValue(item, active, depth + 1));
23
+ }
24
+ const prototype = Object.getPrototypeOf(value);
25
+ if (prototype !== Object.prototype && prototype !== null)
26
+ return false;
27
+ if (Object.getOwnPropertySymbols(value).length)
28
+ return false;
29
+ return Object.values(value).every((item) => validateJsonValue(item, active, depth + 1));
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ finally {
35
+ active.delete(value);
36
+ }
37
+ }
2
38
  export function defineRenderer(definition) {
3
39
  if (!definition || typeof definition.mount !== "function") {
4
40
  throw new TypeError("Renderer mount must be a function");
@@ -14,6 +50,7 @@ export function toRendererProps(context) {
14
50
  locale: context.locale,
15
51
  readonly: context.readonly,
16
52
  theme: context.theme,
53
+ presentation: context.presentation,
17
54
  action: (action, input = {}) => context.bridge.invoke(action, input),
18
55
  runtime: context.bridge,
19
56
  };
@@ -36,7 +73,7 @@ export function createRendererPreviewProps(options) {
36
73
  invoke: async (action, input) => options.onAction?.(action, input) ?? {},
37
74
  reply: async () => undefined,
38
75
  resume: async () => undefined,
39
- host: async () => undefined,
76
+ host: async () => null,
40
77
  };
41
78
  return toRendererProps({
42
79
  protocol: RENDERER_PROTOCOL,
@@ -52,6 +89,7 @@ export function createRendererPreviewProps(options) {
52
89
  locale: options.locale || "zh-CN",
53
90
  readonly: Boolean(options.readonly),
54
91
  theme: {},
92
+ presentation: options.presentation || { mode: "user" },
55
93
  bridge,
56
94
  });
57
95
  }
@@ -80,30 +118,60 @@ export function observeRendererContainer(element, listener) {
80
118
  return () => observer.disconnect();
81
119
  }
82
120
  export function createRendererActionState(onChange) {
83
- let current = { key: "", status: "idle" };
84
- const update = (next) => {
85
- current = next;
86
- onChange?.(Object.freeze({ ...next }));
121
+ let current = Object.freeze({ key: "", status: "idle" });
122
+ const states = new Map();
123
+ const latestRequestByAction = new Map();
124
+ let sequence = 0;
125
+ let latestRequest = 0;
126
+ const snapshot = () => Object.freeze(Object.fromEntries(Array.from(states, ([key, state]) => [key, state])));
127
+ const update = (request, next) => {
128
+ if (latestRequestByAction.get(next.key) !== request)
129
+ return;
130
+ const frozen = Object.freeze({ ...next });
131
+ states.set(next.key, frozen);
132
+ if (request === latestRequest)
133
+ current = frozen;
134
+ onChange?.(current, snapshot());
87
135
  };
88
136
  return {
89
137
  get value() {
90
138
  return current;
91
139
  },
140
+ get values() {
141
+ return snapshot();
142
+ },
143
+ get(action) {
144
+ return states.get(action) || Object.freeze({ key: action, status: "idle" });
145
+ },
92
146
  async invoke(bridge, action, input) {
93
- update({ key: action, status: "running" });
147
+ const request = ++sequence;
148
+ latestRequest = request;
149
+ latestRequestByAction.set(action, request);
150
+ update(request, { key: action, status: "running" });
94
151
  try {
95
152
  const result = await bridge.invoke(action, input);
96
- update({ key: action, status: "completed", result });
153
+ update(request, { key: action, status: "completed", result });
97
154
  return result;
98
155
  }
99
156
  catch (cause) {
100
157
  const error = cause instanceof Error ? cause : new Error(String(cause));
101
- update({ key: action, status: "failed", error });
158
+ update(request, { key: action, status: "failed", error });
102
159
  throw error;
103
160
  }
104
161
  },
105
- reset() {
106
- update({ key: "", status: "idle" });
162
+ reset(action) {
163
+ if (action) {
164
+ latestRequestByAction.delete(action);
165
+ states.delete(action);
166
+ if (current.key === action)
167
+ current = Object.freeze({ key: "", status: "idle" });
168
+ }
169
+ else {
170
+ latestRequestByAction.clear();
171
+ states.clear();
172
+ current = Object.freeze({ key: "", status: "idle" });
173
+ }
174
+ onChange?.(current, snapshot());
107
175
  },
108
176
  };
109
177
  }
package/dist/vite.d.ts CHANGED
@@ -6,6 +6,7 @@ export type RendererSourcePaths = {
6
6
  };
7
7
  export type RendererViteOptions = {
8
8
  framework: RendererFramework;
9
+ mode?: "development" | "production";
9
10
  root?: string;
10
11
  outDir?: string;
11
12
  source?: Partial<RendererSourcePaths>;
package/dist/vite.js CHANGED
@@ -61,12 +61,14 @@ export function rendererEntryPlugin(options) {
61
61
  }
62
62
  export function createRendererViteConfig(options) {
63
63
  const root = resolve(options.root || process.cwd());
64
+ const mode = options.mode || "production";
64
65
  return {
65
66
  root,
67
+ mode,
66
68
  configFile: false,
67
69
  appType: "spa",
68
70
  define: {
69
- "process.env.NODE_ENV": JSON.stringify("production"),
71
+ "process.env.NODE_ENV": JSON.stringify(mode),
70
72
  },
71
73
  plugins: [
72
74
  ...(options.plugins || []),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dingyi-tech/chat-renderer-sdk",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Framework-neutral custom message Renderer contract for Dingyi AI Platform",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -40,7 +40,7 @@
40
40
  "clean": "node scripts/clean.mjs",
41
41
  "prebuild": "npm run clean",
42
42
  "build": "tsc -p tsconfig.json",
43
- "check": "tsc -p tsconfig.json --noEmit && npm run build && node scripts/check-vite-api.mjs",
43
+ "check": "tsc -p tsconfig.json --noEmit && npm run build && node scripts/check-runtime.mjs && node scripts/check-vite-api.mjs",
44
44
  "prepack": "npm run build && node scripts/check-dist.mjs"
45
45
  },
46
46
  "publishConfig": {
@@ -54,10 +54,18 @@
54
54
  "vite": ">=6 <9"
55
55
  },
56
56
  "peerDependenciesMeta": {
57
- "react": { "optional": true },
58
- "react-dom": { "optional": true },
59
- "vue": { "optional": true },
60
- "vite": { "optional": true }
57
+ "react": {
58
+ "optional": true
59
+ },
60
+ "react-dom": {
61
+ "optional": true
62
+ },
63
+ "vue": {
64
+ "optional": true
65
+ },
66
+ "vite": {
67
+ "optional": true
68
+ }
61
69
  },
62
70
  "devDependencies": {
63
71
  "@types/node": "^22.0.0",