xiaozuoassistant 0.1.41

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 (56) hide show
  1. package/README.md +90 -0
  2. package/bin/cli.js +429 -0
  3. package/config.json +36 -0
  4. package/dist/client/assets/browser-ponyfill-DNlTAU2D.js +2 -0
  5. package/dist/client/assets/index-BPATxdcV.js +153 -0
  6. package/dist/client/assets/index-CgL5gMVL.css +1 -0
  7. package/dist/client/favicon.svg +4 -0
  8. package/dist/client/index.html +354 -0
  9. package/dist/client/locales/en/translation.json +77 -0
  10. package/dist/client/locales/zh/translation.json +77 -0
  11. package/dist/server/agents/office.js +23 -0
  12. package/dist/server/app.js +50 -0
  13. package/dist/server/channels/base-channel.js +13 -0
  14. package/dist/server/channels/create-channels.js +18 -0
  15. package/dist/server/channels/dingtalk.js +83 -0
  16. package/dist/server/channels/feishu.js +95 -0
  17. package/dist/server/channels/telegram.js +53 -0
  18. package/dist/server/channels/terminal.js +49 -0
  19. package/dist/server/channels/web.js +45 -0
  20. package/dist/server/channels/wechat.js +107 -0
  21. package/dist/server/config/loader.js +73 -0
  22. package/dist/server/config/prompts.js +12 -0
  23. package/dist/server/core/agents/manager.js +22 -0
  24. package/dist/server/core/agents/runtime.js +85 -0
  25. package/dist/server/core/brain.js +131 -0
  26. package/dist/server/core/event-bus.js +24 -0
  27. package/dist/server/core/logger.js +71 -0
  28. package/dist/server/core/memories/manager.js +115 -0
  29. package/dist/server/core/memories/short-term.js +128 -0
  30. package/dist/server/core/memories/structured.js +109 -0
  31. package/dist/server/core/memories/vector.js +138 -0
  32. package/dist/server/core/memory.js +2 -0
  33. package/dist/server/core/plugin-manager.js +112 -0
  34. package/dist/server/core/plugin.js +1 -0
  35. package/dist/server/core/scheduler.js +24 -0
  36. package/dist/server/core/types.js +1 -0
  37. package/dist/server/index.js +318 -0
  38. package/dist/server/llm/openai.js +23 -0
  39. package/dist/server/routes/auth.js +28 -0
  40. package/dist/server/server/create-http.js +17 -0
  41. package/dist/server/server.js +29 -0
  42. package/dist/server/skills/base-skill.js +16 -0
  43. package/dist/server/skills/create-agent.js +58 -0
  44. package/dist/server/skills/delegate.js +39 -0
  45. package/dist/server/skills/file-system.js +137 -0
  46. package/dist/server/skills/list-agents.js +24 -0
  47. package/dist/server/skills/office-excel.js +84 -0
  48. package/dist/server/skills/office-ppt.js +58 -0
  49. package/dist/server/skills/office-word.js +90 -0
  50. package/dist/server/skills/registry.js +27 -0
  51. package/dist/server/skills/search.js +31 -0
  52. package/dist/server/skills/system-time.js +27 -0
  53. package/package.json +116 -0
  54. package/public/favicon.svg +4 -0
  55. package/public/locales/en/translation.json +77 -0
  56. package/public/locales/zh/translation.json +77 -0
@@ -0,0 +1,29 @@
1
+ /**
2
+ * local server entry file, for local development
3
+ */
4
+ import app from './app.js';
5
+ /**
6
+ * start server with port
7
+ */
8
+ const PORT = process.env.PORT || 3001;
9
+ const server = app.listen(PORT, () => {
10
+ console.log(`Server ready on port ${PORT}`);
11
+ });
12
+ /**
13
+ * close server
14
+ */
15
+ process.on('SIGTERM', () => {
16
+ console.log('SIGTERM signal received');
17
+ server.close(() => {
18
+ console.log('Server closed');
19
+ process.exit(0);
20
+ });
21
+ });
22
+ process.on('SIGINT', () => {
23
+ console.log('SIGINT signal received');
24
+ server.close(() => {
25
+ console.log('Server closed');
26
+ process.exit(0);
27
+ });
28
+ });
29
+ export default app;
@@ -0,0 +1,16 @@
1
+ export class BaseSkill {
2
+ validate(params) {
3
+ // 这里可以添加基于 JSON Schema 的验证逻辑
4
+ return true;
5
+ }
6
+ toJSON() {
7
+ return {
8
+ type: 'function',
9
+ function: {
10
+ name: this.name,
11
+ description: this.description,
12
+ parameters: this.parameters
13
+ }
14
+ };
15
+ }
16
+ }
@@ -0,0 +1,58 @@
1
+ import { BaseSkill } from '../skills/base-skill.js';
2
+ import { AgentRuntime } from '../core/agents/runtime.js';
3
+ import { agentManager } from '../core/agents/manager.js';
4
+ import { skillRegistry } from '../skills/registry.js';
5
+ export class CreateAgentSkill extends BaseSkill {
6
+ constructor() {
7
+ super(...arguments);
8
+ this.name = 'create_agent';
9
+ this.description = 'Create a new specialized agent dynamically. Use this when the user wants to define a new role or expert.';
10
+ this.parameters = {
11
+ type: 'object',
12
+ properties: {
13
+ name: {
14
+ type: 'string',
15
+ description: 'The unique name of the agent (e.g., "coding_expert", "translator")'
16
+ },
17
+ description: {
18
+ type: 'string',
19
+ description: 'A brief description of what this agent does'
20
+ },
21
+ systemPrompt: {
22
+ type: 'string',
23
+ description: 'The system instructions/persona for the agent'
24
+ },
25
+ skills: {
26
+ type: 'array',
27
+ items: {
28
+ type: 'string'
29
+ },
30
+ description: 'List of existing skill names to assign to this agent (e.g., ["search", "read_file"])'
31
+ }
32
+ },
33
+ required: ['name', 'description', 'systemPrompt']
34
+ };
35
+ }
36
+ async execute(args) {
37
+ if (agentManager.getAgent(args.name)) {
38
+ return { error: `Agent '${args.name}' already exists.` };
39
+ }
40
+ const assignedSkills = [];
41
+ if (args.skills) {
42
+ for (const skillName of args.skills) {
43
+ const skill = skillRegistry.getSkill(skillName);
44
+ if (skill) {
45
+ assignedSkills.push(skill);
46
+ }
47
+ }
48
+ }
49
+ const newAgent = new AgentRuntime({
50
+ name: args.name,
51
+ description: args.description,
52
+ systemPrompt: args.systemPrompt,
53
+ skills: assignedSkills
54
+ });
55
+ agentManager.registerAgent(newAgent);
56
+ return { success: true, message: `Agent '${args.name}' created successfully.` };
57
+ }
58
+ }
@@ -0,0 +1,39 @@
1
+ import { BaseSkill } from '../skills/base-skill.js';
2
+ import { agentManager } from '../core/agents/manager.js';
3
+ export class DelegateSkill extends BaseSkill {
4
+ constructor() {
5
+ super(...arguments);
6
+ this.name = 'delegate_task';
7
+ this.description = 'Delegate a complex task to a specialized agent. Use this when the user asks for something that requires specific expertise (e.g., "office_agent" for documents).';
8
+ this.parameters = {
9
+ type: 'object',
10
+ properties: {
11
+ agentName: {
12
+ type: 'string',
13
+ description: 'The name of the agent to delegate to (e.g., "office_agent")',
14
+ enum: ['office_agent'] // Dynamically updated ideally, but hardcoded for now or fetch from manager
15
+ },
16
+ task: {
17
+ type: 'string',
18
+ description: 'The detailed task description for the agent'
19
+ }
20
+ },
21
+ required: ['agentName', 'task']
22
+ };
23
+ }
24
+ async execute(args) {
25
+ const agent = agentManager.getAgent(args.agentName);
26
+ if (!agent) {
27
+ return { error: `Agent '${args.agentName}' not found. Available agents: ${agentManager.getAllAgents().map(a => a.name).join(', ')}` };
28
+ }
29
+ console.log(`[DelegateSkill] Delegating to ${args.agentName}: ${args.task}`);
30
+ try {
31
+ // We start a fresh conversation for the sub-agent for now (stateless delegation)
32
+ const response = await agent.process([], args.task);
33
+ return { result: response, from: args.agentName };
34
+ }
35
+ catch (e) {
36
+ return { error: `Delegation failed: ${e.message}` };
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,137 @@
1
+ import { BaseSkill } from './base-skill.js';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ export class ListDirectorySkill extends BaseSkill {
5
+ constructor() {
6
+ super(...arguments);
7
+ this.name = 'fs_list_directory';
8
+ this.description = 'List files and directories in a given path';
9
+ this.parameters = {
10
+ type: 'object',
11
+ properties: {
12
+ path: {
13
+ type: 'string',
14
+ description: 'The absolute path to the directory to list. Defaults to current working directory.'
15
+ }
16
+ },
17
+ required: []
18
+ };
19
+ }
20
+ async execute(params) {
21
+ const dirPath = params.path ? path.resolve(params.path) : process.cwd();
22
+ try {
23
+ const files = await fs.readdir(dirPath, { withFileTypes: true });
24
+ return files.map(file => ({
25
+ name: file.name,
26
+ type: file.isDirectory() ? 'directory' : 'file'
27
+ }));
28
+ }
29
+ catch (error) {
30
+ return `Error listing directory: ${error.message}`;
31
+ }
32
+ }
33
+ }
34
+ export class ReadFileSkill extends BaseSkill {
35
+ constructor() {
36
+ super(...arguments);
37
+ this.name = 'fs_read_file';
38
+ this.description = 'Read the content of a file';
39
+ this.parameters = {
40
+ type: 'object',
41
+ properties: {
42
+ path: {
43
+ type: 'string',
44
+ description: 'The absolute path to the file to read'
45
+ },
46
+ encoding: {
47
+ type: 'string',
48
+ description: 'The encoding to use (default: utf-8)',
49
+ enum: ['utf-8', 'base64']
50
+ }
51
+ },
52
+ required: ['path']
53
+ };
54
+ }
55
+ async execute(params) {
56
+ const filePath = path.resolve(params.path);
57
+ const encoding = params.encoding || 'utf-8';
58
+ try {
59
+ const content = await fs.readFile(filePath, { encoding: encoding });
60
+ return content;
61
+ }
62
+ catch (error) {
63
+ return `Error reading file: ${error.message}`;
64
+ }
65
+ }
66
+ }
67
+ export class WriteFileSkill extends BaseSkill {
68
+ constructor() {
69
+ super(...arguments);
70
+ this.name = 'fs_write_file';
71
+ this.description = 'Write content to a file (overwrites existing file)';
72
+ this.parameters = {
73
+ type: 'object',
74
+ properties: {
75
+ path: {
76
+ type: 'string',
77
+ description: 'The absolute path to the file to write'
78
+ },
79
+ content: {
80
+ type: 'string',
81
+ description: 'The content to write to the file'
82
+ }
83
+ },
84
+ required: ['path', 'content']
85
+ };
86
+ }
87
+ async execute(params) {
88
+ const filePath = path.resolve(params.path);
89
+ try {
90
+ // Ensure directory exists
91
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
92
+ await fs.writeFile(filePath, params.content, 'utf-8');
93
+ return `Successfully wrote to ${filePath}`;
94
+ }
95
+ catch (error) {
96
+ return `Error writing file: ${error.message}`;
97
+ }
98
+ }
99
+ }
100
+ export class DeleteFileSkill extends BaseSkill {
101
+ constructor() {
102
+ super(...arguments);
103
+ this.name = 'fs_delete_file';
104
+ this.description = 'Delete a file or directory';
105
+ this.parameters = {
106
+ type: 'object',
107
+ properties: {
108
+ path: {
109
+ type: 'string',
110
+ description: 'The absolute path to the file or directory to delete'
111
+ },
112
+ recursive: {
113
+ type: 'boolean',
114
+ description: 'Whether to delete recursively (for directories). Default: false'
115
+ }
116
+ },
117
+ required: ['path']
118
+ };
119
+ }
120
+ async execute(params) {
121
+ const filePath = path.resolve(params.path);
122
+ const recursive = params.recursive || false;
123
+ try {
124
+ const stats = await fs.stat(filePath);
125
+ if (stats.isDirectory()) {
126
+ await fs.rm(filePath, { recursive, force: true });
127
+ }
128
+ else {
129
+ await fs.unlink(filePath);
130
+ }
131
+ return `Successfully deleted ${filePath}`;
132
+ }
133
+ catch (error) {
134
+ return `Error deleting file: ${error.message}`;
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,24 @@
1
+ import { BaseSkill } from '../skills/base-skill.js';
2
+ import { agentManager } from '../core/agents/manager.js';
3
+ export class ListAgentsSkill extends BaseSkill {
4
+ constructor() {
5
+ super(...arguments);
6
+ this.name = 'list_agents';
7
+ this.description = 'List all available agents and their descriptions.';
8
+ this.parameters = {
9
+ type: 'object',
10
+ properties: {},
11
+ required: []
12
+ };
13
+ }
14
+ async execute() {
15
+ const agents = agentManager.getAllAgents();
16
+ return {
17
+ agents: agents.map(a => ({
18
+ name: a.name,
19
+ description: a.description,
20
+ tools: a.getToolsDefinition().map(t => t.function.name)
21
+ }))
22
+ };
23
+ }
24
+ }
@@ -0,0 +1,84 @@
1
+ import { BaseSkill } from './base-skill.js';
2
+ import xlsx from 'xlsx';
3
+ import fs from 'fs';
4
+ export class ReadExcelSkill extends BaseSkill {
5
+ constructor() {
6
+ super(...arguments);
7
+ this.name = 'read_excel_file';
8
+ this.description = 'Read data from a Microsoft Excel (.xlsx) file as JSON';
9
+ this.parameters = {
10
+ type: 'object',
11
+ properties: {
12
+ file_path: {
13
+ type: 'string',
14
+ description: 'The absolute path to the .xlsx file'
15
+ },
16
+ sheet_name: {
17
+ type: 'string',
18
+ description: 'Optional: specific sheet name to read. If omitted, reads the first sheet.'
19
+ }
20
+ },
21
+ required: ['file_path']
22
+ };
23
+ }
24
+ async execute(args) {
25
+ try {
26
+ if (!fs.existsSync(args.file_path)) {
27
+ return { error: `File not found: ${args.file_path}` };
28
+ }
29
+ const workbook = xlsx.readFile(args.file_path);
30
+ const sheetName = args.sheet_name || workbook.SheetNames[0];
31
+ if (!workbook.Sheets[sheetName]) {
32
+ return { error: `Sheet "${sheetName}" not found. Available sheets: ${workbook.SheetNames.join(', ')}` };
33
+ }
34
+ const sheet = workbook.Sheets[sheetName];
35
+ const data = xlsx.utils.sheet_to_json(sheet);
36
+ return {
37
+ sheet: sheetName,
38
+ data: data
39
+ };
40
+ }
41
+ catch (error) {
42
+ return { error: `Failed to read Excel file: ${error.message}` };
43
+ }
44
+ }
45
+ }
46
+ export class CreateExcelSkill extends BaseSkill {
47
+ constructor() {
48
+ super(...arguments);
49
+ this.name = 'create_excel_file';
50
+ this.description = 'Create a new Microsoft Excel (.xlsx) file with data';
51
+ this.parameters = {
52
+ type: 'object',
53
+ properties: {
54
+ file_path: {
55
+ type: 'string',
56
+ description: 'The absolute path where the .xlsx file should be saved'
57
+ },
58
+ data: {
59
+ type: 'array',
60
+ items: { type: 'object' },
61
+ description: 'Array of objects representing rows of data'
62
+ },
63
+ sheet_name: {
64
+ type: 'string',
65
+ description: 'Optional: name of the sheet. Defaults to "Sheet1".'
66
+ }
67
+ },
68
+ required: ['file_path', 'data']
69
+ };
70
+ }
71
+ async execute(args) {
72
+ try {
73
+ const workbook = xlsx.utils.book_new();
74
+ const sheet = xlsx.utils.json_to_sheet(args.data);
75
+ const sheetName = args.sheet_name || 'Sheet1';
76
+ xlsx.utils.book_append_sheet(workbook, sheet, sheetName);
77
+ xlsx.writeFile(workbook, args.file_path);
78
+ return { success: true, message: `Excel file created at ${args.file_path}` };
79
+ }
80
+ catch (error) {
81
+ return { error: `Failed to create Excel file: ${error.message}` };
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,58 @@
1
+ import { BaseSkill } from './base-skill.js';
2
+ import pptxgen from 'pptxgenjs';
3
+ export class CreatePptxSkill extends BaseSkill {
4
+ constructor() {
5
+ super(...arguments);
6
+ this.name = 'create_pptx_file';
7
+ this.description = 'Create a new Microsoft PowerPoint (.pptx) file with slides';
8
+ this.parameters = {
9
+ type: 'object',
10
+ properties: {
11
+ file_path: {
12
+ type: 'string',
13
+ description: 'The absolute path where the .pptx file should be saved'
14
+ },
15
+ slides: {
16
+ type: 'array',
17
+ items: {
18
+ type: 'object',
19
+ properties: {
20
+ title: { type: 'string' },
21
+ content: { type: 'string' } // Simple text content for now
22
+ }
23
+ },
24
+ description: 'Array of slide objects with title and content'
25
+ },
26
+ author: {
27
+ type: 'string',
28
+ description: 'Optional author name'
29
+ }
30
+ },
31
+ required: ['file_path', 'slides']
32
+ };
33
+ }
34
+ async execute(args) {
35
+ try {
36
+ // Handle different import styles (ESM/CJS interop)
37
+ const PptxGenJS = pptxgen.default || pptxgen;
38
+ const pres = new PptxGenJS();
39
+ if (args.author) {
40
+ pres.author = args.author;
41
+ }
42
+ for (const slideData of args.slides) {
43
+ const slide = pres.addSlide();
44
+ if (slideData.title) {
45
+ slide.addText(slideData.title, { x: 1, y: 1, w: '80%', h: 1, fontSize: 24, align: 'center', bold: true });
46
+ }
47
+ if (slideData.content) {
48
+ slide.addText(slideData.content, { x: 1, y: 2.5, w: '80%', h: 3, fontSize: 18 });
49
+ }
50
+ }
51
+ await pres.writeFile({ fileName: args.file_path });
52
+ return { success: true, message: `PowerPoint file created at ${args.file_path}` };
53
+ }
54
+ catch (error) {
55
+ return { error: `Failed to create PowerPoint file: ${error.message}` };
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,90 @@
1
+ import { BaseSkill } from './base-skill.js';
2
+ import mammoth from 'mammoth';
3
+ import officegen from 'officegen';
4
+ import fs from 'fs';
5
+ export class ReadWordSkill extends BaseSkill {
6
+ constructor() {
7
+ super(...arguments);
8
+ this.name = 'read_word_file';
9
+ this.description = 'Read text content from a Microsoft Word (.docx) file';
10
+ this.parameters = {
11
+ type: 'object',
12
+ properties: {
13
+ file_path: {
14
+ type: 'string',
15
+ description: 'The absolute path to the .docx file'
16
+ }
17
+ },
18
+ required: ['file_path']
19
+ };
20
+ }
21
+ async execute(args) {
22
+ try {
23
+ if (!fs.existsSync(args.file_path)) {
24
+ return { error: `File not found: ${args.file_path}` };
25
+ }
26
+ const result = await mammoth.extractRawText({ path: args.file_path });
27
+ return { content: result.value };
28
+ }
29
+ catch (error) {
30
+ return { error: `Failed to read Word file: ${error.message}` };
31
+ }
32
+ }
33
+ }
34
+ export class CreateWordSkill extends BaseSkill {
35
+ constructor() {
36
+ super(...arguments);
37
+ this.name = 'create_word_file';
38
+ this.description = 'Create a new Microsoft Word (.docx) file with text content';
39
+ this.parameters = {
40
+ type: 'object',
41
+ properties: {
42
+ file_path: {
43
+ type: 'string',
44
+ description: 'The absolute path where the .docx file should be saved'
45
+ },
46
+ content: {
47
+ type: 'string',
48
+ description: 'The text content to write into the document'
49
+ },
50
+ title: {
51
+ type: 'string',
52
+ description: 'Optional title for the document'
53
+ }
54
+ },
55
+ required: ['file_path', 'content']
56
+ };
57
+ }
58
+ async execute(args) {
59
+ return new Promise((resolve, reject) => {
60
+ try {
61
+ const docx = officegen('docx');
62
+ docx.on('error', (err) => {
63
+ resolve({ error: `Officegen error: ${err}` });
64
+ });
65
+ if (args.title) {
66
+ const pObj = docx.createP();
67
+ pObj.addText(args.title, { bold: true, font_size: 24 });
68
+ }
69
+ const paragraphs = args.content.split('\n');
70
+ for (const para of paragraphs) {
71
+ if (para.trim()) {
72
+ const pObj = docx.createP();
73
+ pObj.addText(para);
74
+ }
75
+ }
76
+ const out = fs.createWriteStream(args.file_path);
77
+ out.on('error', (err) => {
78
+ resolve({ error: `File write error: ${err}` });
79
+ });
80
+ out.on('close', () => {
81
+ resolve({ success: true, message: `Word file created at ${args.file_path}` });
82
+ });
83
+ docx.generate(out);
84
+ }
85
+ catch (error) {
86
+ resolve({ error: `Failed to create Word file: ${error.message}` });
87
+ }
88
+ });
89
+ }
90
+ }
@@ -0,0 +1,27 @@
1
+ class SkillRegistry {
2
+ constructor() {
3
+ this.skills = new Map();
4
+ }
5
+ static getInstance() {
6
+ if (!SkillRegistry.instance) {
7
+ SkillRegistry.instance = new SkillRegistry();
8
+ }
9
+ return SkillRegistry.instance;
10
+ }
11
+ register(skill) {
12
+ if (this.skills.has(skill.name)) {
13
+ console.warn(`Skill ${skill.name} is already registered. Overwriting.`);
14
+ }
15
+ this.skills.set(skill.name, skill);
16
+ }
17
+ getSkill(name) {
18
+ return this.skills.get(name);
19
+ }
20
+ getAllSkills() {
21
+ return Array.from(this.skills.values());
22
+ }
23
+ getToolsDefinition() {
24
+ return this.getAllSkills().map(skill => skill.toJSON());
25
+ }
26
+ }
27
+ export const skillRegistry = SkillRegistry.getInstance();
@@ -0,0 +1,31 @@
1
+ import { BaseSkill } from './base-skill.js';
2
+ export class SearchSkill extends BaseSkill {
3
+ constructor() {
4
+ super(...arguments);
5
+ this.name = 'web_search';
6
+ this.description = 'Search the web for information';
7
+ this.parameters = {
8
+ type: 'object',
9
+ properties: {
10
+ query: {
11
+ type: 'string',
12
+ description: 'The search query'
13
+ }
14
+ },
15
+ required: ['query']
16
+ };
17
+ }
18
+ async execute(params) {
19
+ const { query } = params;
20
+ // 这里可以集成真实的搜索 API,如 Google Search API 或 SerpApi
21
+ // 为了简化,我们返回一个模拟结果
22
+ return {
23
+ results: [
24
+ {
25
+ title: `Result for ${query}`,
26
+ snippet: `This is a simulated search result for "${query}". In a real implementation, this would call a search engine API.`
27
+ }
28
+ ]
29
+ };
30
+ }
31
+ }
@@ -0,0 +1,27 @@
1
+ import { BaseSkill } from './base-skill.js';
2
+ export class SystemTimeSkill extends BaseSkill {
3
+ constructor() {
4
+ super(...arguments);
5
+ this.name = 'get_system_time';
6
+ this.description = 'Get the current system time';
7
+ this.parameters = {
8
+ type: 'object',
9
+ properties: {
10
+ format: {
11
+ type: 'string',
12
+ description: 'The format of the time (e.g., "iso", "locale")',
13
+ enum: ['iso', 'locale']
14
+ }
15
+ },
16
+ required: []
17
+ };
18
+ }
19
+ async execute(params) {
20
+ const format = params.format || 'iso';
21
+ const now = new Date();
22
+ if (format === 'locale') {
23
+ return now.toLocaleString();
24
+ }
25
+ return now.toISOString();
26
+ }
27
+ }