@zhin.js/core 1.0.16 → 1.0.18
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/CHANGELOG.md +19 -0
- package/REFACTORING_COMPLETE.md +178 -0
- package/REFACTORING_STATUS.md +263 -0
- package/lib/adapter.d.ts +44 -19
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +81 -50
- package/lib/adapter.js.map +1 -1
- package/lib/bot.d.ts +7 -12
- package/lib/bot.d.ts.map +1 -1
- package/lib/built/adapter-process.d.ts +36 -0
- package/lib/built/adapter-process.d.ts.map +1 -0
- package/lib/built/adapter-process.js +77 -0
- package/lib/built/adapter-process.js.map +1 -0
- package/lib/built/command.d.ts +46 -0
- package/lib/built/command.d.ts.map +1 -0
- package/lib/built/command.js +54 -0
- package/lib/built/command.js.map +1 -0
- package/lib/built/component.d.ts +42 -0
- package/lib/built/component.d.ts.map +1 -0
- package/lib/built/component.js +66 -0
- package/lib/built/component.js.map +1 -0
- package/lib/built/config.d.ts +31 -0
- package/lib/built/config.d.ts.map +1 -0
- package/lib/built/config.js +141 -0
- package/lib/built/config.js.map +1 -0
- package/lib/built/cron.d.ts +53 -0
- package/lib/built/cron.d.ts.map +1 -0
- package/lib/built/cron.js +79 -0
- package/lib/built/cron.js.map +1 -0
- package/lib/built/database.d.ts +17 -0
- package/lib/built/database.d.ts.map +1 -0
- package/lib/built/database.js +28 -0
- package/lib/built/database.js.map +1 -0
- package/lib/{permissions.d.ts → built/permission.d.ts} +5 -10
- package/lib/built/permission.d.ts.map +1 -0
- package/lib/{permissions.js → built/permission.js} +11 -10
- package/lib/built/permission.js.map +1 -0
- package/lib/command.d.ts +18 -7
- package/lib/command.d.ts.map +1 -1
- package/lib/command.js +36 -15
- package/lib/command.js.map +1 -1
- package/lib/component.d.ts +1 -1
- package/lib/component.d.ts.map +1 -1
- package/lib/component.js.map +1 -1
- package/lib/cron.d.ts +4 -12
- package/lib/cron.d.ts.map +1 -1
- package/lib/cron.js +33 -64
- package/lib/cron.js.map +1 -1
- package/lib/index.d.ts +11 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +14 -4
- package/lib/index.js.map +1 -1
- package/lib/jsx-runtime.d.ts +2 -2
- package/lib/jsx.d.ts +2 -3
- package/lib/jsx.d.ts.map +1 -1
- package/lib/jsx.js.map +1 -1
- package/lib/message.d.ts +4 -7
- package/lib/message.d.ts.map +1 -1
- package/lib/message.js.map +1 -1
- package/lib/plugin.d.ts +164 -51
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +520 -137
- package/lib/plugin.js.map +1 -1
- package/lib/prompt.d.ts +1 -1
- package/lib/prompt.d.ts.map +1 -1
- package/lib/prompt.js +2 -1
- package/lib/prompt.js.map +1 -1
- package/lib/types.d.ts +33 -33
- package/lib/types.d.ts.map +1 -1
- package/lib/utils.d.ts +16 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +166 -66
- package/lib/utils.js.map +1 -1
- package/package.json +17 -11
- package/src/adapter.ts +131 -80
- package/src/bot.ts +8 -13
- package/src/built/adapter-process.ts +77 -0
- package/src/built/command.ts +102 -0
- package/src/built/component.ts +111 -0
- package/src/built/config.ts +126 -0
- package/src/built/cron.ts +140 -0
- package/src/built/database.ts +38 -0
- package/src/{permissions.ts → built/permission.ts} +9 -12
- package/src/command.ts +48 -20
- package/src/component.ts +2 -3
- package/src/cron.ts +35 -70
- package/src/index.ts +15 -5
- package/src/jsx.ts +2 -3
- package/src/message.ts +3 -4
- package/src/plugin.ts +671 -184
- package/src/prompt.ts +4 -3
- package/src/types.ts +41 -35
- package/src/utils.ts +418 -296
- package/test/minimal-bot.ts +31 -0
- package/test/stress-test.ts +123 -0
- package/tests/command.test.ts +124 -44
- package/ASYNC-JSX-SUPPORT.md +0 -173
- package/lib/app.d.ts +0 -191
- package/lib/app.d.ts.map +0 -1
- package/lib/app.js +0 -604
- package/lib/app.js.map +0 -1
- package/lib/config.d.ts +0 -54
- package/lib/config.d.ts.map +0 -1
- package/lib/config.js +0 -308
- package/lib/config.js.map +0 -1
- package/lib/log-transport.d.ts +0 -37
- package/lib/log-transport.d.ts.map +0 -1
- package/lib/log-transport.js +0 -136
- package/lib/log-transport.js.map +0 -1
- package/lib/permissions.d.ts.map +0 -1
- package/lib/permissions.js.map +0 -1
- package/src/app.ts +0 -772
- package/src/config.ts +0 -397
- package/src/log-transport.ts +0 -163
- package/tests/app.test.ts +0 -265
- package/tests/permissions.test.ts +0 -358
- package/tests/plugin.test.ts +0 -234
- package/tests/prompt.test.ts +0 -223
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { App } from '../src/app';
|
|
2
|
+
import { LogLevel } from '@zhin.js/logger';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
async function runMinimalBot() {
|
|
10
|
+
const app = new App({
|
|
11
|
+
log_level: LogLevel.INFO,
|
|
12
|
+
plugin_dirs: [path.join(__dirname, 'plugins')],
|
|
13
|
+
plugins: [],
|
|
14
|
+
bots: [],
|
|
15
|
+
debug: true
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
console.log('Starting Minimal Bot...');
|
|
19
|
+
await app.start();
|
|
20
|
+
console.log('Minimal Bot Started');
|
|
21
|
+
|
|
22
|
+
// Simulate a message
|
|
23
|
+
// await app.receiveMessage(...)
|
|
24
|
+
|
|
25
|
+
await app.stop();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (process.argv[1] === __filename) {
|
|
29
|
+
runMinimalBot().catch(console.error);
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { App } from '../src/app';
|
|
2
|
+
import { LogLevel } from '@zhin.js/logger';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
// import { Message } from '../src/message'; // Message is a type/namespace, not a class
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
async function runStressTest() {
|
|
12
|
+
const PLUGIN_COUNT = 50;
|
|
13
|
+
const MSG_COUNT = 10000;
|
|
14
|
+
const WORK_DIR = path.join(__dirname, 'stress_plugins');
|
|
15
|
+
|
|
16
|
+
console.log('Starting Core Stress Test');
|
|
17
|
+
|
|
18
|
+
// Setup workspace
|
|
19
|
+
if (fs.existsSync(WORK_DIR)) fs.rmSync(WORK_DIR, { recursive: true, force: true });
|
|
20
|
+
fs.mkdirSync(WORK_DIR, { recursive: true });
|
|
21
|
+
|
|
22
|
+
// Generate plugins
|
|
23
|
+
let loadedCount = 0;
|
|
24
|
+
for (let i = 0; i < PLUGIN_COUNT; i++) {
|
|
25
|
+
fs.writeFileSync(path.join(WORK_DIR, `plugin-${i}.ts`), `
|
|
26
|
+
import { useContext } from '${path.resolve(__dirname, '../src/plugin').replace(/\\/g, '/')}';
|
|
27
|
+
|
|
28
|
+
export function install(ctx) {
|
|
29
|
+
ctx.on('message.receive', (msg) => {
|
|
30
|
+
// console.log('Plugin ${i} received message');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
ctx.middleware(async (msg, next) => {
|
|
34
|
+
await next();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const CONFIG_FILE = path.join(WORK_DIR, 'zhin.test.yaml');
|
|
41
|
+
// Write config to file to avoid reload overriding it with defaults
|
|
42
|
+
const config = {
|
|
43
|
+
log_level: LogLevel.WARN,
|
|
44
|
+
plugin_dirs: [WORK_DIR],
|
|
45
|
+
plugins: Array.from({ length: PLUGIN_COUNT }, (_, i) => `plugin-${i}`),
|
|
46
|
+
bots: [],
|
|
47
|
+
debug: false
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Simple YAML stringify since we can't require 'yaml' easily in ESM without import
|
|
51
|
+
const yamlString = `
|
|
52
|
+
log_level: ${config.log_level}
|
|
53
|
+
debug: ${config.debug}
|
|
54
|
+
plugin_dirs:
|
|
55
|
+
${config.plugin_dirs.map(d => ` - ${d}`).join('\n')}
|
|
56
|
+
plugins:
|
|
57
|
+
${config.plugins.map(p => ` - ${p}`).join('\n')}
|
|
58
|
+
bots: []
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
fs.writeFileSync(CONFIG_FILE, yamlString);
|
|
62
|
+
|
|
63
|
+
const app = new App(CONFIG_FILE);
|
|
64
|
+
|
|
65
|
+
console.log('Starting App...');
|
|
66
|
+
const startBoot = performance.now();
|
|
67
|
+
await app.start();
|
|
68
|
+
console.log(`App started in ${(performance.now() - startBoot).toFixed(2)}ms`);
|
|
69
|
+
|
|
70
|
+
// Verify plugins loaded
|
|
71
|
+
const loadedPlugins = app.hmrManager.dependencyList.length; // App itself + plugins
|
|
72
|
+
console.log(`Loaded Dependencies: ${loadedPlugins}`);
|
|
73
|
+
if (loadedPlugins < PLUGIN_COUNT) {
|
|
74
|
+
console.warn(`WARNING: Only ${loadedPlugins} dependencies loaded (expected >= ${PLUGIN_COUNT})`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const initialMem = process.memoryUsage();
|
|
78
|
+
console.log(`Initial RSS: ${(initialMem.rss / 1024 / 1024).toFixed(2)} MB`);
|
|
79
|
+
|
|
80
|
+
console.log(`Sending ${MSG_COUNT} messages...`);
|
|
81
|
+
const startMsg = performance.now();
|
|
82
|
+
|
|
83
|
+
// Mock message factory
|
|
84
|
+
const createMockMsg = (id: number) => ({
|
|
85
|
+
$id: `msg-${id}`,
|
|
86
|
+
$adapter: 'mock',
|
|
87
|
+
$bot: 'bot1',
|
|
88
|
+
$content: [{ type: 'text', data: { text: 'hello' } }],
|
|
89
|
+
$sender: { id: 'user1' },
|
|
90
|
+
$channel: { id: 'group1', type: 'group' as const },
|
|
91
|
+
$timestamp: Date.now(),
|
|
92
|
+
$raw: 'hello',
|
|
93
|
+
$reply: async () => 'reply-id',
|
|
94
|
+
$recall: async () => {},
|
|
95
|
+
// Custom fields usually added by adapters
|
|
96
|
+
raw_message: 'hello',
|
|
97
|
+
message_type: 'group',
|
|
98
|
+
sender: { id: 'user1' },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < MSG_COUNT; i++) {
|
|
102
|
+
const msg = createMockMsg(i);
|
|
103
|
+
await app.receiveMessage(msg as any);
|
|
104
|
+
|
|
105
|
+
if (i % 1000 === 0) {
|
|
106
|
+
const mem = process.memoryUsage();
|
|
107
|
+
process.stdout.write(`\rMsgs: ${i}, RSS: ${(mem.rss / 1024 / 1024).toFixed(2)} MB`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(`\nProcessed ${MSG_COUNT} messages in ${(performance.now() - startMsg).toFixed(2)}ms`);
|
|
112
|
+
console.log(`Avg: ${((performance.now() - startMsg) / MSG_COUNT).toFixed(3)}ms/msg`);
|
|
113
|
+
|
|
114
|
+
const finalMem = process.memoryUsage();
|
|
115
|
+
console.log(`Final RSS: ${(finalMem.rss / 1024 / 1024).toFixed(2)} MB`);
|
|
116
|
+
console.log(`Memory Delta: ${((finalMem.rss - initialMem.rss) / 1024 / 1024).toFixed(2)} MB`);
|
|
117
|
+
|
|
118
|
+
await app.stop();
|
|
119
|
+
fs.rmSync(WORK_DIR, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
runStressTest().catch(console.error);
|
|
123
|
+
|
package/tests/command.test.ts
CHANGED
|
@@ -49,55 +49,58 @@ vi.mock('segment-matcher', () => {
|
|
|
49
49
|
})
|
|
50
50
|
|
|
51
51
|
// Mock App with permissions
|
|
52
|
+
const mockPermissionService = {
|
|
53
|
+
check: vi.fn(async (perm: string, message: any) => {
|
|
54
|
+
if (perm === 'adapter(discord)') {
|
|
55
|
+
return message.$adapter === 'discord'
|
|
56
|
+
}
|
|
57
|
+
if (perm === 'adapter(telegram)') {
|
|
58
|
+
return message.$adapter === 'telegram'
|
|
59
|
+
}
|
|
60
|
+
if (perm === 'adapter(email)') {
|
|
61
|
+
return message.$adapter === 'email'
|
|
62
|
+
}
|
|
63
|
+
if (perm === 'adapter(test)') {
|
|
64
|
+
return message.$adapter === 'test'
|
|
65
|
+
}
|
|
66
|
+
return true
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
const mockApp = {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (permission === 'adapter(discord)') {
|
|
59
|
-
return message.$adapter === 'discord'
|
|
60
|
-
}
|
|
61
|
-
if (permission === 'adapter(telegram)') {
|
|
62
|
-
return message.$adapter === 'telegram'
|
|
63
|
-
}
|
|
64
|
-
if (permission === 'adapter(email)') {
|
|
65
|
-
return message.$adapter === 'email'
|
|
66
|
-
}
|
|
67
|
-
if (permission === 'adapter(test)') {
|
|
68
|
-
return message.$adapter === 'test'
|
|
69
|
-
}
|
|
70
|
-
return true
|
|
71
|
-
})
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
}
|
|
71
|
+
contextIsReady: vi.fn((name: string) => name === 'permission'),
|
|
72
|
+
inject: vi.fn((name: string) => {
|
|
73
|
+
if (name === 'permission') return mockPermissionService
|
|
74
|
+
return null
|
|
75
|
+
})
|
|
75
76
|
} as any
|
|
76
77
|
|
|
77
78
|
// 为多个权限测试创建特殊的 mock app
|
|
79
|
+
const multiPermitPermissionService = {
|
|
80
|
+
check: vi.fn(async (perm: string, message: any) => {
|
|
81
|
+
// 对于多个权限,只要有一个匹配就返回 true
|
|
82
|
+
if (perm === 'adapter(discord)' && message.$adapter === 'discord') {
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
if (perm === 'adapter(telegram)' && message.$adapter === 'telegram') {
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
if (perm === 'adapter(email)' && message.$adapter === 'email') {
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
if (perm === 'adapter(test)' && message.$adapter === 'test') {
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
94
|
+
return false
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
78
98
|
const multiPermitMockApp = {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (permission === 'adapter(discord)' && message.$adapter === 'discord') {
|
|
85
|
-
return true
|
|
86
|
-
}
|
|
87
|
-
if (permission === 'adapter(telegram)' && message.$adapter === 'telegram') {
|
|
88
|
-
return true
|
|
89
|
-
}
|
|
90
|
-
if (permission === 'adapter(email)' && message.$adapter === 'email') {
|
|
91
|
-
return true
|
|
92
|
-
}
|
|
93
|
-
if (permission === 'adapter(test)' && message.$adapter === 'test') {
|
|
94
|
-
return true
|
|
95
|
-
}
|
|
96
|
-
return false
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
}
|
|
99
|
+
contextIsReady: vi.fn((name: string) => name === 'permission'),
|
|
100
|
+
inject: vi.fn((name: string) => {
|
|
101
|
+
if (name === 'permission') return multiPermitPermissionService
|
|
102
|
+
return null
|
|
103
|
+
})
|
|
101
104
|
} as any
|
|
102
105
|
|
|
103
106
|
describe('Command系统测试', () => {
|
|
@@ -611,6 +614,83 @@ describe('Command系统测试', () => {
|
|
|
611
614
|
})
|
|
612
615
|
})
|
|
613
616
|
|
|
617
|
+
describe('帮助系统测试', () => {
|
|
618
|
+
it('应该正确设置和获取描述信息', () => {
|
|
619
|
+
const command = new MessageCommand('help')
|
|
620
|
+
.desc('这是命令描述', '可以有多行描述')
|
|
621
|
+
|
|
622
|
+
expect(command.helpInfo.desc).toEqual(['这是命令描述', '可以有多行描述'])
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
it('应该正确设置和获取用法信息', () => {
|
|
626
|
+
const command = new MessageCommand('help')
|
|
627
|
+
.usage('help', 'help <command>')
|
|
628
|
+
|
|
629
|
+
expect(command.helpInfo.usage).toEqual(['help', 'help <command>'])
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
it('应该正确设置和获取示例信息', () => {
|
|
633
|
+
const command = new MessageCommand('help')
|
|
634
|
+
.examples('help', 'help echo', 'help admin')
|
|
635
|
+
|
|
636
|
+
expect(command.helpInfo.examples).toEqual(['help', 'help echo', 'help admin'])
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
it('应该支持链式调用设置帮助信息', () => {
|
|
640
|
+
const command = new MessageCommand('test')
|
|
641
|
+
.desc('测试命令', '用于测试功能')
|
|
642
|
+
.usage('test', 'test <arg>')
|
|
643
|
+
.examples('test', 'test hello')
|
|
644
|
+
.action(() => 'Test response')
|
|
645
|
+
|
|
646
|
+
expect(command.helpInfo.pattern).toBe('test')
|
|
647
|
+
expect(command.helpInfo.desc).toEqual(['测试命令', '用于测试功能'])
|
|
648
|
+
expect(command.helpInfo.usage).toEqual(['test', 'test <arg>'])
|
|
649
|
+
expect(command.helpInfo.examples).toEqual(['test', 'test hello'])
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
it('应该正确生成帮助文本', () => {
|
|
653
|
+
const command = new MessageCommand('greet')
|
|
654
|
+
.desc('打招呼命令')
|
|
655
|
+
.usage('greet <name>')
|
|
656
|
+
.examples('greet Alice')
|
|
657
|
+
|
|
658
|
+
const help = command.help
|
|
659
|
+
|
|
660
|
+
expect(help).toContain('greet')
|
|
661
|
+
expect(help).toContain('打招呼命令')
|
|
662
|
+
expect(help).toContain('greet <name>')
|
|
663
|
+
expect(help).toContain('greet Alice')
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
it('应该处理没有帮助信息的情况', () => {
|
|
667
|
+
const command = new MessageCommand('simple')
|
|
668
|
+
|
|
669
|
+
expect(command.helpInfo.desc).toEqual([])
|
|
670
|
+
expect(command.helpInfo.usage).toEqual([])
|
|
671
|
+
expect(command.helpInfo.examples).toEqual([])
|
|
672
|
+
expect(command.help).toBe('simple')
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
it('应该正确返回 helpInfo 对象结构', () => {
|
|
676
|
+
const command = new MessageCommand('info')
|
|
677
|
+
.desc('信息命令')
|
|
678
|
+
.usage('info')
|
|
679
|
+
.examples('info')
|
|
680
|
+
|
|
681
|
+
const helpInfo = command.helpInfo
|
|
682
|
+
|
|
683
|
+
expect(helpInfo).toHaveProperty('pattern')
|
|
684
|
+
expect(helpInfo).toHaveProperty('desc')
|
|
685
|
+
expect(helpInfo).toHaveProperty('usage')
|
|
686
|
+
expect(helpInfo).toHaveProperty('examples')
|
|
687
|
+
expect(typeof helpInfo.pattern).toBe('string')
|
|
688
|
+
expect(Array.isArray(helpInfo.desc)).toBe(true)
|
|
689
|
+
expect(Array.isArray(helpInfo.usage)).toBe(true)
|
|
690
|
+
expect(Array.isArray(helpInfo.examples)).toBe(true)
|
|
691
|
+
})
|
|
692
|
+
})
|
|
693
|
+
|
|
614
694
|
describe('权限系统测试', () => {
|
|
615
695
|
it('应该正确处理权限检查失败', async () => {
|
|
616
696
|
const command = new MessageCommand('admin')
|
package/ASYNC-JSX-SUPPORT.md
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
# 异步 JSX 组件支持
|
|
2
|
-
|
|
3
|
-
## 概述
|
|
4
|
-
|
|
5
|
-
Zhin Core 现在原生支持异步 JSX 组件,允许你像使用普通组件一样使用异步组件,无需额外的类型断言或注释。
|
|
6
|
-
|
|
7
|
-
## 核心改动
|
|
8
|
-
|
|
9
|
-
### 1. 类型系统扩展
|
|
10
|
-
|
|
11
|
-
**`packages/core/src/jsx.ts`**:
|
|
12
|
-
- 修改 `JSX.Element` 类型为联合类型,支持 `Promise<SendContent>`
|
|
13
|
-
- `renderJSX` 函数自动检测并 await Promise 返回值
|
|
14
|
-
- 错误时自动捕获并返回错误信息
|
|
15
|
-
|
|
16
|
-
**`packages/core/src/message.ts`**:
|
|
17
|
-
- `MessageComponent` 类型支持异步组件函数
|
|
18
|
-
|
|
19
|
-
### 2. 运行时支持
|
|
20
|
-
|
|
21
|
-
**自动 Promise 处理**:
|
|
22
|
-
```typescript
|
|
23
|
-
export async function renderJSX(element: MessageComponent<any>, context?: ComponentContext): Promise<SendContent> {
|
|
24
|
-
try {
|
|
25
|
-
// ... 组件渲染逻辑
|
|
26
|
-
const result = await component(element.data, context || {} as ComponentContext);
|
|
27
|
-
|
|
28
|
-
// 如果组件返回 Promise,自动 await
|
|
29
|
-
if (result && typeof result === 'object' && 'then' in result) {
|
|
30
|
-
return await result;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return result;
|
|
34
|
-
} catch (error) {
|
|
35
|
-
// 渲染错误时返回错误信息
|
|
36
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
37
|
-
return `❌ 组件渲染失败: ${errorMessage}`;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
**子组件 Promise 处理**:
|
|
43
|
-
```typescript
|
|
44
|
-
async function renderChildren(children: JSXChildren, context?: ComponentContext): Promise<SendContent> {
|
|
45
|
-
// ...
|
|
46
|
-
// 如果子元素是 Promise,自动 await
|
|
47
|
-
if (children && typeof children === 'object' && 'then' in children) {
|
|
48
|
-
try {
|
|
49
|
-
return await children;
|
|
50
|
-
} catch (error) {
|
|
51
|
-
return `❌ 组件渲染失败: ${errorMessage}`;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## 使用方式
|
|
58
|
-
|
|
59
|
-
### 定义异步组件
|
|
60
|
-
|
|
61
|
-
```tsx
|
|
62
|
-
import { defineComponent, addComponent } from 'zhin.js';
|
|
63
|
-
|
|
64
|
-
const AsyncComponent = defineComponent(async function AsyncComponent({ userId }: { userId: string }) {
|
|
65
|
-
// 执行异步操作
|
|
66
|
-
const user = await fetchUserFromDatabase(userId);
|
|
67
|
-
const profile = await fetchUserProfile(userId);
|
|
68
|
-
|
|
69
|
-
return `👤 ${user.name}\n📧 ${profile.email}`;
|
|
70
|
-
}, 'AsyncComponent');
|
|
71
|
-
|
|
72
|
-
addComponent(AsyncComponent);
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### 在 JSX 中使用(现在完全类型安全)
|
|
76
|
-
|
|
77
|
-
```tsx
|
|
78
|
-
addCommand(
|
|
79
|
-
new MessageCommand('用户 <userId:text>')
|
|
80
|
-
.action(async (message, result) => {
|
|
81
|
-
// ✅ 直接使用 JSX 语法,无需 @ts-expect-error
|
|
82
|
-
return <AsyncComponent userId={result.params.userId} />
|
|
83
|
-
})
|
|
84
|
-
);
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### 嵌套异步组件
|
|
88
|
-
|
|
89
|
-
```tsx
|
|
90
|
-
const UserProfile = defineComponent(async function UserProfile({ userId }: { userId: string }) {
|
|
91
|
-
const user = await fetchUser(userId);
|
|
92
|
-
|
|
93
|
-
// 嵌套使用其他异步组件
|
|
94
|
-
return (
|
|
95
|
-
<div>
|
|
96
|
-
<h1>{user.name}</h1>
|
|
97
|
-
<AsyncComponent userId={userId} />
|
|
98
|
-
</div>
|
|
99
|
-
);
|
|
100
|
-
}, 'UserProfile');
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
## 错误处理
|
|
104
|
-
|
|
105
|
-
异步组件中的错误会自动被捕获并返回友好的错误信息:
|
|
106
|
-
|
|
107
|
-
```tsx
|
|
108
|
-
const FailingComponent = defineComponent(async function FailingComponent() {
|
|
109
|
-
throw new Error('数据加载失败');
|
|
110
|
-
}, 'FailingComponent');
|
|
111
|
-
|
|
112
|
-
// 使用时会自动返回: "❌ 组件渲染失败: 数据加载失败"
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## 性能考虑
|
|
116
|
-
|
|
117
|
-
- **自动 await**:框架自动检测 Promise 并等待,无额外开销
|
|
118
|
-
- **并行渲染**:多个异步组件可以并行加载(使用 `Promise.all`)
|
|
119
|
-
- **错误隔离**:单个组件错误不会影响整体渲染
|
|
120
|
-
|
|
121
|
-
## 迁移指南
|
|
122
|
-
|
|
123
|
-
如果你之前使用了 `@ts-expect-error` 或直接函数调用:
|
|
124
|
-
|
|
125
|
-
```tsx
|
|
126
|
-
// ❌ 旧方式(已废弃)
|
|
127
|
-
return await ShareMusic({ platform: 'qq', musicId: '123' });
|
|
128
|
-
|
|
129
|
-
// ✅ 新方式(推荐)
|
|
130
|
-
return <ShareMusic platform="qq" musicId="123" />
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## TypeScript 类型
|
|
134
|
-
|
|
135
|
-
```typescript
|
|
136
|
-
// JSX.Element 现在支持 Promise
|
|
137
|
-
declare global {
|
|
138
|
-
namespace JSX {
|
|
139
|
-
type Element = MessageComponent<any> | Promise<MessageComponent<any>> | Promise<SendContent>
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// MessageComponent 支持异步函数
|
|
144
|
-
export type MessageComponent<T extends object> = {
|
|
145
|
-
type: Component<T & {children?: SendContent}> | ((props: T & {children?: SendContent}) => Promise<SendContent>)
|
|
146
|
-
data: T
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
## 测试
|
|
151
|
-
|
|
152
|
-
确保你的异步组件正确工作:
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
import { describe, it, expect } from 'vitest';
|
|
156
|
-
|
|
157
|
-
describe('Async Components', () => {
|
|
158
|
-
it('should render async component', async () => {
|
|
159
|
-
const result = await renderJSX(<AsyncComponent userId="123" />);
|
|
160
|
-
expect(result).toBe('👤 User Name\n📧 user@example.com');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('should handle errors gracefully', async () => {
|
|
164
|
-
const result = await renderJSX(<FailingComponent />);
|
|
165
|
-
expect(result).toMatch(/❌ 组件渲染失败/);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
---
|
|
171
|
-
|
|
172
|
-
**版本**: 1.0.15+
|
|
173
|
-
**文档更新**: 2025-11-19
|