@yuhere/js-jsonrpc2 0.0.0-stage → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +277 -2
  2. package/SKILL.md +237 -0
  3. package/dist/cli/cli.d.ts +3 -0
  4. package/dist/cli/cli.d.ts.map +1 -0
  5. package/dist/cli/cli.js +132 -0
  6. package/dist/cli/cli.js.map +1 -0
  7. package/dist/cli/demo-methods.d.ts +10 -0
  8. package/dist/cli/demo-methods.d.ts.map +1 -0
  9. package/dist/cli/demo-methods.js +477 -0
  10. package/dist/cli/demo-methods.js.map +1 -0
  11. package/dist/cli/express.d.ts +2 -0
  12. package/dist/cli/express.d.ts.map +1 -0
  13. package/dist/cli/express.js +42 -0
  14. package/dist/cli/express.js.map +1 -0
  15. package/dist/cli/stdio.d.ts +2 -0
  16. package/dist/cli/stdio.d.ts.map +1 -0
  17. package/dist/cli/stdio.js +5 -0
  18. package/dist/cli/stdio.js.map +1 -0
  19. package/dist/core/Repository.d.ts +24 -0
  20. package/dist/core/Repository.d.ts.map +1 -0
  21. package/dist/core/Repository.js +153 -0
  22. package/dist/core/Repository.js.map +1 -0
  23. package/dist/core/convert.d.ts +4 -0
  24. package/dist/core/convert.d.ts.map +1 -0
  25. package/dist/core/convert.js +29 -0
  26. package/dist/core/convert.js.map +1 -0
  27. package/dist/core/descriptor.d.ts +27 -0
  28. package/dist/core/descriptor.d.ts.map +1 -0
  29. package/dist/core/descriptor.js +83 -0
  30. package/dist/core/descriptor.js.map +1 -0
  31. package/dist/core/index.d.ts +4 -0
  32. package/dist/core/index.d.ts.map +1 -0
  33. package/dist/core/index.js +3 -0
  34. package/dist/core/index.js.map +1 -0
  35. package/dist/core/permission.d.ts +4 -0
  36. package/dist/core/permission.d.ts.map +1 -0
  37. package/dist/core/permission.js +18 -0
  38. package/dist/core/permission.js.map +1 -0
  39. package/dist/core/spec-check.d.ts +3 -0
  40. package/dist/core/spec-check.d.ts.map +1 -0
  41. package/dist/core/spec-check.js +11 -0
  42. package/dist/core/spec-check.js.map +1 -0
  43. package/dist/core/system-methods.d.ts +3 -0
  44. package/dist/core/system-methods.d.ts.map +1 -0
  45. package/dist/core/system-methods.js +76 -0
  46. package/dist/core/system-methods.js.map +1 -0
  47. package/dist/errors/RpcError.d.ts +9 -0
  48. package/dist/errors/RpcError.d.ts.map +1 -0
  49. package/dist/errors/RpcError.js +23 -0
  50. package/dist/errors/RpcError.js.map +1 -0
  51. package/dist/errors/codes.d.ts +7 -0
  52. package/dist/errors/codes.d.ts.map +1 -0
  53. package/dist/errors/codes.js +7 -0
  54. package/dist/errors/codes.js.map +1 -0
  55. package/dist/errors/index.d.ts +3 -0
  56. package/dist/errors/index.d.ts.map +1 -0
  57. package/dist/errors/index.js +3 -0
  58. package/dist/errors/index.js.map +1 -0
  59. package/dist/index.d.ts +18 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +17 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/schema/index.d.ts +4 -0
  64. package/dist/schema/index.d.ts.map +1 -0
  65. package/dist/schema/index.js +4 -0
  66. package/dist/schema/index.js.map +1 -0
  67. package/dist/schema/json/index.d.ts +19 -0
  68. package/dist/schema/json/index.d.ts.map +1 -0
  69. package/dist/schema/json/index.js +6 -0
  70. package/dist/schema/json/index.js.map +1 -0
  71. package/dist/schema/json/marker.d.ts +23 -0
  72. package/dist/schema/json/marker.d.ts.map +1 -0
  73. package/dist/schema/json/marker.js +27 -0
  74. package/dist/schema/json/marker.js.map +1 -0
  75. package/dist/schema/json/primitives.d.ts +202 -0
  76. package/dist/schema/json/primitives.d.ts.map +1 -0
  77. package/dist/schema/json/primitives.js +598 -0
  78. package/dist/schema/json/primitives.js.map +1 -0
  79. package/dist/schema/primitives.d.ts +25 -0
  80. package/dist/schema/primitives.d.ts.map +1 -0
  81. package/dist/schema/primitives.js +153 -0
  82. package/dist/schema/primitives.js.map +1 -0
  83. package/dist/schema/rpc.d.ts +21 -0
  84. package/dist/schema/rpc.d.ts.map +1 -0
  85. package/dist/schema/rpc.js +17 -0
  86. package/dist/schema/rpc.js.map +1 -0
  87. package/dist/schema/z/protocol.d.ts +9 -0
  88. package/dist/schema/z/protocol.d.ts.map +1 -0
  89. package/dist/schema/z/protocol.js +9 -0
  90. package/dist/schema/z/protocol.js.map +1 -0
  91. package/dist/schema/z/register.d.ts +9 -0
  92. package/dist/schema/z/register.d.ts.map +1 -0
  93. package/dist/schema/z/register.js +21 -0
  94. package/dist/schema/z/register.js.map +1 -0
  95. package/dist/stream/index.d.ts +3 -0
  96. package/dist/stream/index.d.ts.map +1 -0
  97. package/dist/stream/index.js +2 -0
  98. package/dist/stream/index.js.map +1 -0
  99. package/dist/stream/stringify-stream.d.ts +15 -0
  100. package/dist/stream/stringify-stream.d.ts.map +1 -0
  101. package/dist/stream/stringify-stream.js +322 -0
  102. package/dist/stream/stringify-stream.js.map +1 -0
  103. package/dist/stream/stringify.d.ts +19 -0
  104. package/dist/stream/stringify.d.ts.map +1 -0
  105. package/dist/stream/stringify.js +217 -0
  106. package/dist/stream/stringify.js.map +1 -0
  107. package/dist/transport/express/body.d.ts +9 -0
  108. package/dist/transport/express/body.d.ts.map +1 -0
  109. package/dist/transport/express/body.js +28 -0
  110. package/dist/transport/express/body.js.map +1 -0
  111. package/dist/transport/express/index.d.ts +3 -0
  112. package/dist/transport/express/index.d.ts.map +1 -0
  113. package/dist/transport/express/index.js +2 -0
  114. package/dist/transport/express/index.js.map +1 -0
  115. package/dist/transport/express/middleware.d.ts +12 -0
  116. package/dist/transport/express/middleware.d.ts.map +1 -0
  117. package/dist/transport/express/middleware.js +73 -0
  118. package/dist/transport/express/middleware.js.map +1 -0
  119. package/dist/transport/express/respond.d.ts +4 -0
  120. package/dist/transport/express/respond.d.ts.map +1 -0
  121. package/dist/transport/express/respond.js +125 -0
  122. package/dist/transport/express/respond.js.map +1 -0
  123. package/dist/transport/stdio/index.d.ts +2 -0
  124. package/dist/transport/stdio/index.d.ts.map +1 -0
  125. package/dist/transport/stdio/index.js +2 -0
  126. package/dist/transport/stdio/index.js.map +1 -0
  127. package/dist/transport/stdio/middleware.d.ts +34 -0
  128. package/dist/transport/stdio/middleware.d.ts.map +1 -0
  129. package/dist/transport/stdio/middleware.js +274 -0
  130. package/dist/transport/stdio/middleware.js.map +1 -0
  131. package/dist/transport/ws/index.d.ts +3 -0
  132. package/dist/transport/ws/index.d.ts.map +1 -0
  133. package/dist/transport/ws/index.js +3 -0
  134. package/dist/transport/ws/index.js.map +1 -0
  135. package/dist/transport/ws/middleware.d.ts +25 -0
  136. package/dist/transport/ws/middleware.d.ts.map +1 -0
  137. package/dist/transport/ws/middleware.js +209 -0
  138. package/dist/transport/ws/middleware.js.map +1 -0
  139. package/dist/transport/ws/standalone.d.ts +22 -0
  140. package/dist/transport/ws/standalone.d.ts.map +1 -0
  141. package/dist/transport/ws/standalone.js +39 -0
  142. package/dist/transport/ws/standalone.js.map +1 -0
  143. package/dist/types.d.ts +75 -0
  144. package/dist/types.d.ts.map +1 -0
  145. package/dist/types.js +2 -0
  146. package/dist/types.js.map +1 -0
  147. package/htdoc/assets/index-CQr-lnlD.js +9286 -0
  148. package/htdoc/assets/index-CQr-lnlD.js.map +1 -0
  149. package/htdoc/assets/index-hcP2-9jw.css +729 -0
  150. package/htdoc/index.html +13 -0
  151. package/package.json +89 -3
  152. package/webui/index.html +12 -0
  153. package/webui/src/App.tsx +56 -0
  154. package/webui/src/api/httpTransport.ts +148 -0
  155. package/webui/src/api/paramForm.ts +79 -0
  156. package/webui/src/api/rpc.ts +57 -0
  157. package/webui/src/api/schema.ts +12 -0
  158. package/webui/src/api/types.ts +16 -0
  159. package/webui/src/api/wsTransport.ts +305 -0
  160. package/webui/src/components/MethodDetail.tsx +43 -0
  161. package/webui/src/components/MethodTree.tsx +118 -0
  162. package/webui/src/components/SchemaForm.tsx +696 -0
  163. package/webui/src/components/SchemaView.tsx +151 -0
  164. package/webui/src/components/TestPanel.tsx +279 -0
  165. package/webui/src/components/Toolbar.tsx +70 -0
  166. package/webui/src/hooks/useMethodDetail.ts +61 -0
  167. package/webui/src/hooks/useMethods.ts +27 -0
  168. package/webui/src/main.tsx +13 -0
  169. package/webui/src/styles/geek.css +729 -0
  170. package/webui/src/theme/ThemeProvider.tsx +79 -0
  171. package/webui/src/vite-env.d.ts +1 -0
  172. package/webui/tsconfig.json +34 -0
  173. package/webui/tsconfig.node.json +13 -0
  174. package/webui/vite.config.ts +15 -0
@@ -0,0 +1,151 @@
1
+ import { useState } from 'react';
2
+ import type { SchemaDescription } from '../api/schema';
3
+
4
+ interface SchemaViewProps {
5
+ schema: SchemaDescription;
6
+ /** 顶层是否默认展开 */
7
+ defaultOpen?: boolean;
8
+ /** 缩进深度(内部递归使用) */
9
+ depth?: number;
10
+ }
11
+
12
+ const KIND_COLORS: Record<string, string> = {
13
+ string: 'var(--syn-string)',
14
+ number: 'var(--syn-number)',
15
+ boolean: 'var(--syn-boolean)',
16
+ literal: 'var(--syn-literal)',
17
+ array: 'var(--syn-container)',
18
+ tuple: 'var(--syn-container)',
19
+ object: 'var(--syn-object)',
20
+ record: 'var(--syn-object)',
21
+ union: 'var(--syn-union)',
22
+ enum: 'var(--syn-union)',
23
+ nullable: 'var(--syn-muted)',
24
+ unknown: 'var(--syn-muted)',
25
+ stream: 'var(--syn-container)',
26
+ };
27
+
28
+ function colorOf(kind: string): string {
29
+ return KIND_COLORS[kind] ?? 'var(--fg)';
30
+ }
31
+
32
+ /** 把 constraints 渲染成紧凑的一行,如:min=1, max=10 */
33
+ function formatConstraints(constraints?: Record<string, unknown>): string {
34
+ if (!constraints) return '';
35
+ const entries = Object.entries(constraints).filter(([, v]) => v !== undefined);
36
+ if (entries.length === 0) return '';
37
+ return entries
38
+ .map(([k, v]) => `${k}=${typeof v === 'string' ? JSON.stringify(v) : JSON.stringify(v)}`)
39
+ .join(', ');
40
+ }
41
+
42
+ interface NodeProps {
43
+ schema: SchemaDescription;
44
+ depth: number;
45
+ defaultOpen: boolean;
46
+ label?: string;
47
+ }
48
+
49
+ function SchemaNode({ schema, depth, defaultOpen, label }: NodeProps) {
50
+ const [open, setOpen] = useState(defaultOpen);
51
+ const hasChildren = schema.children && Object.keys(schema.children).length > 0;
52
+ const constraintsText = formatConstraints(schema.constraints);
53
+
54
+ return (
55
+ <div className="schema-node" style={{ paddingLeft: depth * 14 }}>
56
+ <div className="schema-head" onClick={() => hasChildren && setOpen((o) => !o)}>
57
+ {hasChildren ? (
58
+ <span className="schema-expander">{open ? '▾' : '▸'}</span>
59
+ ) : (
60
+ <span className="schema-expander" />
61
+ )}
62
+ {label !== undefined && <span className="schema-label">{label}:</span>}
63
+ <span className="schema-kind" style={{ color: colorOf(schema.kind) }}>
64
+ {schema.kind}
65
+ </span>
66
+ {schema.kind === 'stream' && <span className="schema-nullable">&lt;stream&gt;</span>}
67
+ {schema.nullable && <span className="schema-nullable">| null</span>}
68
+ {constraintsText && <span className="schema-constraints">{`{ ${constraintsText} }`}</span>}
69
+ {schema.description && (
70
+ <span className="schema-description" title={schema.description}>
71
+ {' '}
72
+ // {schema.description}
73
+ </span>
74
+ )}
75
+ </div>
76
+ {open && hasChildren && (
77
+ <div className="schema-children">{renderChildren(schema, depth + 1)}</div>
78
+ )}
79
+ </div>
80
+ );
81
+ }
82
+
83
+ function renderChildren(schema: SchemaDescription, depth: number): React.ReactNode {
84
+ const c = schema.children ?? {};
85
+ const out: React.ReactNode[] = [];
86
+
87
+ // nullable / stream 包装器
88
+ if (c.inner && isSchemaDescription(c.inner)) {
89
+ out.push(<SchemaNode key="inner" schema={c.inner} depth={depth} defaultOpen />);
90
+ }
91
+
92
+ // array / tuple
93
+ if (Array.isArray(c.items)) {
94
+ (c.items as unknown[]).forEach((item, i) => {
95
+ if (isSchemaDescription(item)) {
96
+ out.push(
97
+ <SchemaNode
98
+ key={`item-${i}`}
99
+ schema={item}
100
+ depth={depth}
101
+ defaultOpen
102
+ label={String(i)}
103
+ />
104
+ );
105
+ }
106
+ });
107
+ } else if (c.items && isSchemaDescription(c.items)) {
108
+ out.push(<SchemaNode key="items" schema={c.items} depth={depth} defaultOpen label="items" />);
109
+ }
110
+
111
+ // union
112
+ if (Array.isArray(c.options)) {
113
+ (c.options as unknown[]).forEach((opt, i) => {
114
+ if (isSchemaDescription(opt)) {
115
+ out.push(
116
+ <SchemaNode
117
+ key={`opt-${i}`}
118
+ schema={opt}
119
+ depth={depth}
120
+ defaultOpen={false}
121
+ label={`option[${i}]`}
122
+ />
123
+ );
124
+ }
125
+ });
126
+ }
127
+
128
+ // object
129
+ if (c.properties && typeof c.properties === 'object' && !Array.isArray(c.properties)) {
130
+ for (const [key, prop] of Object.entries(c.properties)) {
131
+ if (isSchemaDescription(prop)) {
132
+ out.push(<SchemaNode key={`prop-${key}`} schema={prop} depth={depth} defaultOpen label={key} />);
133
+ }
134
+ }
135
+ }
136
+
137
+ // record
138
+ if (c.values && isSchemaDescription(c.values)) {
139
+ out.push(<SchemaNode key="values" schema={c.values} depth={depth} defaultOpen label="values" />);
140
+ }
141
+
142
+ return out;
143
+ }
144
+
145
+ function isSchemaDescription(v: unknown): v is SchemaDescription {
146
+ return !!v && typeof v === 'object' && typeof (v as SchemaDescription).kind === 'string';
147
+ }
148
+
149
+ export function SchemaView({ schema, defaultOpen = true, depth = 0 }: SchemaViewProps) {
150
+ return <SchemaNode schema={schema} depth={depth} defaultOpen={defaultOpen} />;
151
+ }
@@ -0,0 +1,279 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { RPCError, isStreamResult, type RpcClient, type TransportKind } from '../api/rpc';
3
+ import type { MethodDetail } from '../hooks/useMethodDetail';
4
+ import { SchemaForm } from './SchemaForm';
5
+ import { sampleFromSchema } from '../api/paramForm';
6
+ import { absoluteEndpoint } from '../api/httpTransport';
7
+
8
+ interface TestPanelProps {
9
+ method: string;
10
+ detail: MethodDetail;
11
+ rpc: RpcClient;
12
+ transport: TransportKind;
13
+ endpoint: string;
14
+ }
15
+
16
+ type CopyKind = 'curl' | 'fetch';
17
+
18
+ const COPY_OPTIONS: { value: CopyKind; label: string }[] = [
19
+ { value: 'curl', label: '复制 curl' },
20
+ { value: 'fetch', label: '复制 fetch' },
21
+ ];
22
+
23
+ export function TestPanel({
24
+ method,
25
+ detail,
26
+ rpc,
27
+ transport,
28
+ endpoint,
29
+ }: TestPanelProps) {
30
+ const paramSchemas = useMemo(() => detail.signature.slice(1), [detail.signature]);
31
+ const isStreamReturn = detail.signature[0]?.kind === 'stream';
32
+
33
+ const [values, setValues] = useState<unknown[]>([]);
34
+ const [output, setOutput] = useState('');
35
+ const [running, setRunning] = useState(false);
36
+ const [copyKind, setCopyKind] = useState<CopyKind>('curl');
37
+ const [copied, setCopied] = useState(false);
38
+ const [copyError, setCopyError] = useState<string | null>(null);
39
+ const [menuOpen, setMenuOpen] = useState(false);
40
+ const menuRef = useRef<HTMLDivElement>(null);
41
+ const outRef = useRef<HTMLPreElement>(null);
42
+
43
+ useEffect(() => {
44
+ if (!method) return;
45
+ setValues(paramSchemas.map(sampleFromSchema));
46
+ setOutput('');
47
+ }, [method, paramSchemas]);
48
+
49
+ useEffect(() => {
50
+ setCopied(false);
51
+ setCopyError(null);
52
+ setMenuOpen(false);
53
+ }, [method]);
54
+
55
+ // 点击外部关闭菜单
56
+ useEffect(() => {
57
+ if (!menuOpen) return;
58
+ const onDocClick = (e: MouseEvent) => {
59
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
60
+ setMenuOpen(false);
61
+ }
62
+ };
63
+ document.addEventListener('mousedown', onDocClick);
64
+ return () => document.removeEventListener('mousedown', onDocClick);
65
+ }, [menuOpen]);
66
+
67
+ const setValue = (i: number, v: unknown) => {
68
+ setValues((prev) => {
69
+ const next = prev.slice();
70
+ next[i] = v;
71
+ return next;
72
+ });
73
+ };
74
+
75
+ const execute = async () => {
76
+ if (!method) return;
77
+ setRunning(true);
78
+ setOutput('');
79
+ try {
80
+ const result = await rpc.call(method, ...values);
81
+ if (isStreamResult(result)) {
82
+ let idx = 0;
83
+ const lines: string[] = [];
84
+ for await (const value of result) {
85
+ lines.push(`[${idx}] ${JSON.stringify(value, null, 2)}`);
86
+ idx++;
87
+ setOutput(lines.join('\n'));
88
+ }
89
+ if (idx === 0) setOutput('(流已结束,无数据)');
90
+ else lines.push(`(流已结束,共 ${idx} 帧)`), setOutput(lines.join('\n'));
91
+ } else {
92
+ setOutput(JSON.stringify(result, null, 2));
93
+ }
94
+
95
+ if (isStreamReturn) {
96
+ } else {
97
+ const result = await rpc.call(method, ...values);
98
+ setOutput(JSON.stringify(result, null, 2));
99
+ }
100
+ } catch (e) {
101
+ setOutput(formatError(e));
102
+ } finally {
103
+ setRunning(false);
104
+ requestAnimationFrame(() =>
105
+ outRef.current?.scrollIntoView({ block: 'nearest' }),
106
+ );
107
+ }
108
+ };
109
+
110
+ const copyAs = async (kind: CopyKind) => {
111
+ if (!method) return;
112
+ setCopyError(null);
113
+ let code: string;
114
+ try {
115
+ const body = {
116
+ jsonrpc: '2.0' as const,
117
+ method,
118
+ params: values,
119
+ id: 1,
120
+ };
121
+ const url = absoluteEndpoint(endpoint);
122
+ code = kind === 'curl' ? buildCurl(url, body) : buildFetch(url, body);
123
+ } catch (e) {
124
+ setCopyError(e instanceof Error ? e.message : String(e));
125
+ return;
126
+ }
127
+ try {
128
+ await navigator.clipboard.writeText(code);
129
+ setCopied(true);
130
+ setTimeout(() => setCopied(false), 2000);
131
+ } catch (e) {
132
+ setCopyError(
133
+ `复制失败:${e instanceof Error ? e.message : String(e)}`,
134
+ );
135
+ }
136
+ };
137
+
138
+ const canCopy = transport === 'http';
139
+ const currentLabel =
140
+ COPY_OPTIONS.find((o) => o.value === copyKind)?.label ?? '复制';
141
+
142
+ return (
143
+ <section className="test">
144
+ <h3>
145
+ 参数{' '}
146
+ <span className="hint">
147
+ ({paramSchemas.length} 个,按签名自动生成)
148
+ </span>
149
+ </h3>
150
+
151
+ <SchemaForm
152
+ params={paramSchemas}
153
+ values={values}
154
+ onChange={setValue}
155
+ disabled={running}
156
+ />
157
+
158
+ <div className="actions">
159
+ <button onClick={execute} disabled={!method || running}>
160
+ {running ? (isStreamReturn ? '接收中…' : '执行中…') : (isStreamReturn ? '执行并接收流 ▶' : '执行 ▶')}
161
+ </button>
162
+
163
+ <div className="split-button" ref={menuRef}>
164
+ <button
165
+ type="button"
166
+ className="split-button-main"
167
+ onClick={() => void copyAs(copyKind)}
168
+ disabled={!method || !canCopy}
169
+ title={canCopy ? '复制当前调用的等价代码' : '仅 HTTP 传输可生成代码'}
170
+ >
171
+ {copied ? '已复制 ✓' : currentLabel}
172
+ </button>
173
+ <button
174
+ type="button"
175
+ className="split-button-caret"
176
+ onClick={() => setMenuOpen((o) => !o)}
177
+ disabled={!method || !canCopy}
178
+ aria-haspopup="menu"
179
+ aria-expanded={menuOpen}
180
+ title="选择复制格式"
181
+ >
182
+ ▾
183
+ </button>
184
+ {menuOpen && (
185
+ <ul className="split-button-menu" role="menu">
186
+ {COPY_OPTIONS.map((opt) => (
187
+ <li
188
+ key={opt.value}
189
+ role="menuitem"
190
+ onClick={() => {
191
+ setCopyKind(opt.value);
192
+ setMenuOpen(false);
193
+ void copyAs(opt.value);
194
+ }}
195
+ >
196
+ {opt.label}
197
+ {copyKind === opt.value && (
198
+ <span className="check">✓</span>
199
+ )}
200
+ </li>
201
+ ))}
202
+ </ul>
203
+ )}
204
+ </div>
205
+
206
+ {copyError && <span className="error small">{copyError}</span>}
207
+
208
+ <button
209
+ onClick={() => setValues(paramSchemas.map(sampleFromSchema))}
210
+ disabled={!method}
211
+ >
212
+ 重置为示例
213
+ </button>
214
+ <button onClick={() => setOutput('')} disabled={!output}>
215
+ 清空输出
216
+ </button>
217
+ </div>
218
+
219
+ <h3>结果{isStreamReturn && <span className="hint">(流式)</span>}</h3>
220
+ <pre className="output" ref={outRef}>
221
+ {output || '(尚未执行)'}
222
+ </pre>
223
+ </section>
224
+ );
225
+ }
226
+
227
+ // ─────────────────────────────────────────────────────────────
228
+ // 代码片段生成
229
+ // ─────────────────────────────────────────────────────────────
230
+
231
+ interface RpcBody {
232
+ jsonrpc: '2.0';
233
+ method: string;
234
+ params: unknown[];
235
+ id: number;
236
+ }
237
+
238
+ function buildCurl(url: string, body: RpcBody): string {
239
+ const bodyText = JSON.stringify(body, null, 2);
240
+ const quotedBody = `'${bodyText.replace(/'/g, `'\\''`)}'`;
241
+ return [
242
+ `curl -X POST '${url}'`,
243
+ ` -H 'Content-Type: application/json'`,
244
+ ` -d ${quotedBody}`,
245
+ ].join(' \\\n');
246
+ }
247
+
248
+ function buildFetch(url: string, body: RpcBody): string {
249
+ const bodyText = JSON.stringify(body, null, 2);
250
+ const indented = bodyText
251
+ .split('\n')
252
+ .map((line, i) => (i === 0 ? line : ' ' + line))
253
+ .join('\n');
254
+ return [
255
+ 'await fetch(' + JSON.stringify(url) + ', {',
256
+ ` method: 'POST',`,
257
+ ` headers: { 'Content-Type': 'application/json' },`,
258
+ ' body: JSON.stringify(',
259
+ ' ' + indented.split('\n').join('\n '),
260
+ ' ),',
261
+ '});',
262
+ ].join('\n');
263
+ }
264
+
265
+ function formatError(e: unknown): string {
266
+ if (e instanceof RPCError) {
267
+ const { code, message, data } = e.payload;
268
+ const lines = [`Error ${code}: ${message}`];
269
+ if (data !== undefined) {
270
+ lines.push('');
271
+ lines.push('data:');
272
+ lines.push(
273
+ typeof data === 'string' ? data : JSON.stringify(data, null, 2),
274
+ );
275
+ }
276
+ return lines.join('\n');
277
+ }
278
+ return `Error: ${e instanceof Error ? e.message : String(e)}`;
279
+ }
@@ -0,0 +1,70 @@
1
+ import type { TransportKind } from '../api/rpc';
2
+ import { useTheme, type ThemeMode } from '../theme/ThemeProvider';
3
+
4
+ interface ToolbarProps {
5
+ transport: TransportKind;
6
+ onTransportChange: (t: TransportKind) => void;
7
+ onRefresh: () => void;
8
+ loading: boolean;
9
+ error: string | null;
10
+ }
11
+
12
+ const THEME_OPTIONS: { value: ThemeMode; label: string }[] = [
13
+ { value: 'system', label: '跟随系统' },
14
+ { value: 'light', label: '亮' },
15
+ { value: 'dark', label: '暗' },
16
+ ];
17
+
18
+ export function Toolbar({
19
+ transport,
20
+ onTransportChange,
21
+ onRefresh,
22
+ loading,
23
+ error,
24
+ }: ToolbarProps) {
25
+ const { mode, setMode } = useTheme();
26
+
27
+ return (
28
+ <header className="toolbar">
29
+ <h1>JSON-RPC 2.0</h1>
30
+
31
+ <div className="segmented" role="group" aria-label="传输方式">
32
+ <button
33
+ type="button"
34
+ className={transport === 'http' ? 'active' : ''}
35
+ onClick={() => onTransportChange('http')}
36
+ >
37
+ HTTP
38
+ </button>
39
+ <button
40
+ type="button"
41
+ className={transport === 'ws' ? 'active' : ''}
42
+ onClick={() => onTransportChange('ws')}
43
+ >
44
+ WebSocket
45
+ </button>
46
+ </div>
47
+
48
+ <button onClick={onRefresh} disabled={loading}>
49
+ {loading ? '刷新中…' : '刷新方法列表'}
50
+ </button>
51
+
52
+ {error && <span className="error">{error}</span>}
53
+
54
+ {/* 主题下拉,放右上角 */}
55
+ <label className="theme-select" title="主题">
56
+ <span className="theme-select-label">主题</span>
57
+ <select
58
+ value={mode}
59
+ onChange={(e) => setMode(e.target.value as ThemeMode)}
60
+ >
61
+ {THEME_OPTIONS.map((opt) => (
62
+ <option key={opt.value} value={opt.value}>
63
+ {opt.label}
64
+ </option>
65
+ ))}
66
+ </select>
67
+ </label>
68
+ </header>
69
+ );
70
+ }
@@ -0,0 +1,61 @@
1
+ import { useEffect, useState } from 'react';
2
+ import type { RpcClient } from '../api/rpc';
3
+ import type { SchemaDescription } from '../api/schema';
4
+
5
+ export interface MethodDetail {
6
+ help: string;
7
+ /** system.methodSignature 返回的 Schema 列表(每项对应一个参数) */
8
+ signature: SchemaDescription[];
9
+ error: string | null;
10
+ loading: boolean;
11
+ }
12
+
13
+ const EMPTY: MethodDetail = { help: '', signature: [], error: null, loading: false };
14
+
15
+ function isSchemaDescription(v: unknown): v is SchemaDescription {
16
+ return !!v && typeof v === 'object' && typeof (v as SchemaDescription).kind === 'string';
17
+ }
18
+
19
+ export function useMethodDetail(rpc: RpcClient, method: string): MethodDetail {
20
+ const [detail, setDetail] = useState<MethodDetail>(EMPTY);
21
+
22
+ useEffect(() => {
23
+ if (!method) {
24
+ setDetail(EMPTY);
25
+ return;
26
+ }
27
+ let cancelled = false;
28
+ setDetail({ help: '', signature: [], error: null, loading: true });
29
+
30
+ (async () => {
31
+ try {
32
+ const [help, signature] = await Promise.all([
33
+ rpc.call<string>('system.methodHelp', method).catch(() => ''),
34
+ rpc.call<unknown[]>('system.methodSignature', method).catch(() => []),
35
+ ]);
36
+ if (cancelled) return;
37
+ const sigArr = Array.isArray(signature) ? signature.filter(isSchemaDescription) : [];
38
+ setDetail({
39
+ help: typeof help === 'string' ? help : '',
40
+ signature: sigArr,
41
+ error: null,
42
+ loading: false,
43
+ });
44
+ } catch (e) {
45
+ if (cancelled) return;
46
+ setDetail({
47
+ help: '',
48
+ signature: [],
49
+ error: e instanceof Error ? e.message : String(e),
50
+ loading: false,
51
+ });
52
+ }
53
+ })();
54
+
55
+ return () => {
56
+ cancelled = true;
57
+ };
58
+ }, [rpc, method]);
59
+
60
+ return detail;
61
+ }
@@ -0,0 +1,27 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import type { RpcClient } from '../api/rpc';
3
+
4
+ export function useMethods(rpc: RpcClient) {
5
+ const [methods, setMethods] = useState<string[]>([]);
6
+ const [loading, setLoading] = useState(false);
7
+ const [error, setError] = useState<string | null>(null);
8
+
9
+ const refresh = useCallback(async () => {
10
+ setLoading(true);
11
+ setError(null);
12
+ try {
13
+ const list = await rpc.call<string[]>('system.listMethods');
14
+ setMethods(Array.isArray(list) ? [...list].sort() : []);
15
+ } catch (e) {
16
+ setError(e instanceof Error ? e.message : String(e));
17
+ } finally {
18
+ setLoading(false);
19
+ }
20
+ }, [rpc]);
21
+
22
+ useEffect(() => {
23
+ void refresh();
24
+ }, [refresh]);
25
+
26
+ return { methods, loading, error, refresh };
27
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+ import { ThemeProvider } from './theme/ThemeProvider';
5
+ import './styles/geek.css';
6
+
7
+ ReactDOM.createRoot(document.getElementById('root')!).render(
8
+ <React.StrictMode>
9
+ <ThemeProvider>
10
+ <App />
11
+ </ThemeProvider>
12
+ </React.StrictMode>
13
+ );