ideaco 1.1.5
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/.dockerignore +33 -0
- package/.nvmrc +1 -0
- package/ARCHITECTURE.md +394 -0
- package/Dockerfile +50 -0
- package/LICENSE +29 -0
- package/README.md +206 -0
- package/bin/i18n.js +46 -0
- package/bin/ideaco.js +494 -0
- package/deploy.sh +15 -0
- package/docker-compose.yml +30 -0
- package/electron/main.cjs +986 -0
- package/electron/preload.cjs +14 -0
- package/electron/web-backends.cjs +854 -0
- package/jsconfig.json +8 -0
- package/next.config.mjs +34 -0
- package/package.json +134 -0
- package/postcss.config.mjs +6 -0
- package/public/demo/dashboard.png +0 -0
- package/public/demo/employee.png +0 -0
- package/public/demo/messages.png +0 -0
- package/public/demo/office.png +0 -0
- package/public/demo/requirement.png +0 -0
- package/public/logo.jpeg +0 -0
- package/public/logo.png +0 -0
- package/scripts/prepare-electron.js +67 -0
- package/scripts/release.js +76 -0
- package/src/app/api/agents/[agentId]/chat/route.js +70 -0
- package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
- package/src/app/api/agents/[agentId]/route.js +106 -0
- package/src/app/api/avatar/route.js +104 -0
- package/src/app/api/browse-dir/route.js +44 -0
- package/src/app/api/chat/route.js +265 -0
- package/src/app/api/company/factory-reset/route.js +43 -0
- package/src/app/api/company/route.js +82 -0
- package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
- package/src/app/api/departments/route.js +92 -0
- package/src/app/api/group-chat-loop/events/route.js +70 -0
- package/src/app/api/group-chat-loop/route.js +94 -0
- package/src/app/api/mailbox/route.js +100 -0
- package/src/app/api/messages/route.js +14 -0
- package/src/app/api/providers/[id]/configure/route.js +21 -0
- package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
- package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
- package/src/app/api/providers/route.js +11 -0
- package/src/app/api/requirements/route.js +242 -0
- package/src/app/api/secretary/route.js +65 -0
- package/src/app/api/system/cli-backends/route.js +91 -0
- package/src/app/api/system/cron/route.js +110 -0
- package/src/app/api/system/knowledge/route.js +104 -0
- package/src/app/api/system/plugins/route.js +40 -0
- package/src/app/api/system/skills/route.js +46 -0
- package/src/app/api/system/status/route.js +46 -0
- package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
- package/src/app/api/talent-market/[profileId]/route.js +17 -0
- package/src/app/api/talent-market/route.js +26 -0
- package/src/app/api/teams/route.js +773 -0
- package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
- package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
- package/src/app/globals.css +130 -0
- package/src/app/layout.jsx +40 -0
- package/src/app/page.jsx +97 -0
- package/src/components/AgentChatModal.jsx +164 -0
- package/src/components/AgentDetailModal.jsx +425 -0
- package/src/components/AgentSpyModal.jsx +481 -0
- package/src/components/AvatarGrid.jsx +29 -0
- package/src/components/BossProfileModal.jsx +162 -0
- package/src/components/CachedAvatar.jsx +77 -0
- package/src/components/ChatPanel.jsx +219 -0
- package/src/components/ChatShared.jsx +255 -0
- package/src/components/DepartmentDetail.jsx +842 -0
- package/src/components/DepartmentView.jsx +367 -0
- package/src/components/FileReference.jsx +260 -0
- package/src/components/FilesView.jsx +465 -0
- package/src/components/GroupChatView.jsx +799 -0
- package/src/components/Mailbox.jsx +926 -0
- package/src/components/MessagesView.jsx +112 -0
- package/src/components/OnboardingGuide.jsx +209 -0
- package/src/components/OrgTree.jsx +151 -0
- package/src/components/Overview.jsx +391 -0
- package/src/components/PixelOffice.jsx +2281 -0
- package/src/components/ProviderGrid.jsx +551 -0
- package/src/components/ProvidersBoard.jsx +16 -0
- package/src/components/RequirementDetail.jsx +1279 -0
- package/src/components/RequirementsBoard.jsx +187 -0
- package/src/components/SecretarySettings.jsx +295 -0
- package/src/components/SetupWizard.jsx +388 -0
- package/src/components/Sidebar.jsx +169 -0
- package/src/components/SystemMonitor.jsx +808 -0
- package/src/components/TalentMarket.jsx +183 -0
- package/src/components/TeamDetail.jsx +697 -0
- package/src/core/agent/base-agent.js +104 -0
- package/src/core/agent/chat-store.js +602 -0
- package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
- package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
- package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
- package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
- package/src/core/agent/cli-agent/backends/index.js +27 -0
- package/src/core/agent/cli-agent/backends/registry.js +580 -0
- package/src/core/agent/cli-agent/index.js +154 -0
- package/src/core/agent/index.js +60 -0
- package/src/core/agent/llm-agent/client.js +320 -0
- package/src/core/agent/llm-agent/index.js +97 -0
- package/src/core/agent/message-bus.js +211 -0
- package/src/core/agent/session.js +608 -0
- package/src/core/agent/tools.js +596 -0
- package/src/core/agent/web-agent/backends/base-backend.js +180 -0
- package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
- package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
- package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
- package/src/core/agent/web-agent/backends/index.js +91 -0
- package/src/core/agent/web-agent/index.js +278 -0
- package/src/core/agent/web-agent/web-client.js +407 -0
- package/src/core/employee/base-employee.js +1088 -0
- package/src/core/employee/index.js +35 -0
- package/src/core/employee/knowledge.js +327 -0
- package/src/core/employee/lifecycle.js +990 -0
- package/src/core/employee/memory/index.js +642 -0
- package/src/core/employee/memory/store.js +143 -0
- package/src/core/employee/performance.js +224 -0
- package/src/core/employee/secretary.js +625 -0
- package/src/core/employee/skills.js +398 -0
- package/src/core/index.js +38 -0
- package/src/core/organization/company.js +2600 -0
- package/src/core/organization/department.js +737 -0
- package/src/core/organization/group-chat-loop.js +264 -0
- package/src/core/organization/index.js +8 -0
- package/src/core/organization/persistence.js +111 -0
- package/src/core/organization/team.js +267 -0
- package/src/core/organization/workforce/hr.js +377 -0
- package/src/core/organization/workforce/providers.js +468 -0
- package/src/core/organization/workforce/role-archetypes.js +805 -0
- package/src/core/organization/workforce/talent-market.js +205 -0
- package/src/core/prompts.js +532 -0
- package/src/core/requirement.js +1789 -0
- package/src/core/system/audit.js +483 -0
- package/src/core/system/cron.js +449 -0
- package/src/core/system/index.js +7 -0
- package/src/core/system/plugin.js +2183 -0
- package/src/core/utils/json-parse.js +188 -0
- package/src/core/workspace.js +239 -0
- package/src/lib/api-i18n.js +211 -0
- package/src/lib/avatar.js +268 -0
- package/src/lib/client-store.js +1025 -0
- package/src/lib/config-validator.js +483 -0
- package/src/lib/format-time.js +22 -0
- package/src/lib/hooks.js +414 -0
- package/src/lib/i18n.js +134 -0
- package/src/lib/paths.js +23 -0
- package/src/lib/store.js +72 -0
- package/src/locales/de.js +393 -0
- package/src/locales/en.js +1054 -0
- package/src/locales/es.js +393 -0
- package/src/locales/fr.js +393 -0
- package/src/locales/ja.js +501 -0
- package/src/locales/ko.js +513 -0
- package/src/locales/zh.js +828 -0
- package/tailwind.config.mjs +11 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Validator - Schema-based config validation and safe defaults
|
|
3
|
+
*
|
|
4
|
+
* Distilled from OpenClaw's config system (vendor/openclaw/src/config/validation.ts
|
|
5
|
+
* and vendor/openclaw/src/config/schema.ts)
|
|
6
|
+
* Re-implemented as an enterprise "company policy compliance engine"
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Declarative schema definition with type checking
|
|
10
|
+
* - Nested object and array validation
|
|
11
|
+
* - Default value injection
|
|
12
|
+
* - Environment variable interpolation
|
|
13
|
+
* - Validation error aggregation with friendly messages
|
|
14
|
+
* - Runtime config patching with safety checks
|
|
15
|
+
* - Config diffing for change tracking
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Supported config value types
|
|
20
|
+
*/
|
|
21
|
+
export const ConfigType = {
|
|
22
|
+
STRING: 'string',
|
|
23
|
+
NUMBER: 'number',
|
|
24
|
+
BOOLEAN: 'boolean',
|
|
25
|
+
ARRAY: 'array',
|
|
26
|
+
OBJECT: 'object',
|
|
27
|
+
ENUM: 'enum',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validation error entry
|
|
32
|
+
* @typedef {object} ValidationError
|
|
33
|
+
* @property {string} path - Dot-notation path to the invalid field
|
|
34
|
+
* @property {string} message - Human-readable error message
|
|
35
|
+
* @property {*} value - The invalid value
|
|
36
|
+
* @property {string} rule - Which rule was violated
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Schema field definition
|
|
41
|
+
* @typedef {object} SchemaField
|
|
42
|
+
* @property {string} type - ConfigType value
|
|
43
|
+
* @property {string} description - Field description
|
|
44
|
+
* @property {*} default - Default value if not provided
|
|
45
|
+
* @property {boolean} required - Whether the field is required
|
|
46
|
+
* @property {Array} enum - Allowed values (for 'enum' type)
|
|
47
|
+
* @property {number} min - Minimum value (for 'number') or min length (for 'string'/'array')
|
|
48
|
+
* @property {number} max - Maximum value (for 'number') or max length (for 'string'/'array')
|
|
49
|
+
* @property {RegExp|string} pattern - Regex pattern (for 'string')
|
|
50
|
+
* @property {object} items - Schema for array items
|
|
51
|
+
* @property {object} properties - Schema for object properties
|
|
52
|
+
* @property {string} envVar - Environment variable to read from
|
|
53
|
+
* @property {Function} validate - Custom validation function: (value) => string|null
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate a value against a schema field definition
|
|
58
|
+
*
|
|
59
|
+
* @param {*} value - Value to validate
|
|
60
|
+
* @param {SchemaField} field - Schema definition
|
|
61
|
+
* @param {string} path - Current dot-notation path (for error messages)
|
|
62
|
+
* @returns {ValidationError[]}
|
|
63
|
+
*/
|
|
64
|
+
function validateField(value, field, path) {
|
|
65
|
+
const errors = [];
|
|
66
|
+
|
|
67
|
+
// Handle required check
|
|
68
|
+
if (value === undefined || value === null) {
|
|
69
|
+
if (field.required) {
|
|
70
|
+
errors.push({ path, message: `Required field "${path}" is missing`, value, rule: 'required' });
|
|
71
|
+
}
|
|
72
|
+
return errors; // No further validation on missing optional fields
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Type checking
|
|
76
|
+
switch (field.type) {
|
|
77
|
+
case ConfigType.STRING:
|
|
78
|
+
if (typeof value !== 'string') {
|
|
79
|
+
errors.push({ path, message: `"${path}" must be a string, got ${typeof value}`, value, rule: 'type' });
|
|
80
|
+
return errors;
|
|
81
|
+
}
|
|
82
|
+
if (field.min !== undefined && value.length < field.min) {
|
|
83
|
+
errors.push({ path, message: `"${path}" must be at least ${field.min} characters`, value, rule: 'min' });
|
|
84
|
+
}
|
|
85
|
+
if (field.max !== undefined && value.length > field.max) {
|
|
86
|
+
errors.push({ path, message: `"${path}" must be at most ${field.max} characters`, value, rule: 'max' });
|
|
87
|
+
}
|
|
88
|
+
if (field.pattern) {
|
|
89
|
+
const regex = field.pattern instanceof RegExp ? field.pattern : new RegExp(field.pattern);
|
|
90
|
+
if (!regex.test(value)) {
|
|
91
|
+
errors.push({ path, message: `"${path}" does not match required pattern`, value, rule: 'pattern' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
case ConfigType.NUMBER:
|
|
97
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
98
|
+
errors.push({ path, message: `"${path}" must be a number, got ${typeof value}`, value, rule: 'type' });
|
|
99
|
+
return errors;
|
|
100
|
+
}
|
|
101
|
+
if (field.min !== undefined && value < field.min) {
|
|
102
|
+
errors.push({ path, message: `"${path}" must be >= ${field.min}`, value, rule: 'min' });
|
|
103
|
+
}
|
|
104
|
+
if (field.max !== undefined && value > field.max) {
|
|
105
|
+
errors.push({ path, message: `"${path}" must be <= ${field.max}`, value, rule: 'max' });
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case ConfigType.BOOLEAN:
|
|
110
|
+
if (typeof value !== 'boolean') {
|
|
111
|
+
errors.push({ path, message: `"${path}" must be a boolean, got ${typeof value}`, value, rule: 'type' });
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
case ConfigType.ENUM:
|
|
116
|
+
if (!field.enum || !field.enum.includes(value)) {
|
|
117
|
+
errors.push({
|
|
118
|
+
path,
|
|
119
|
+
message: `"${path}" must be one of [${(field.enum || []).join(', ')}], got "${value}"`,
|
|
120
|
+
value,
|
|
121
|
+
rule: 'enum',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
case ConfigType.ARRAY:
|
|
127
|
+
if (!Array.isArray(value)) {
|
|
128
|
+
errors.push({ path, message: `"${path}" must be an array, got ${typeof value}`, value, rule: 'type' });
|
|
129
|
+
return errors;
|
|
130
|
+
}
|
|
131
|
+
if (field.min !== undefined && value.length < field.min) {
|
|
132
|
+
errors.push({ path, message: `"${path}" must have at least ${field.min} items`, value, rule: 'min' });
|
|
133
|
+
}
|
|
134
|
+
if (field.max !== undefined && value.length > field.max) {
|
|
135
|
+
errors.push({ path, message: `"${path}" must have at most ${field.max} items`, value, rule: 'max' });
|
|
136
|
+
}
|
|
137
|
+
// Validate array items
|
|
138
|
+
if (field.items) {
|
|
139
|
+
value.forEach((item, index) => {
|
|
140
|
+
errors.push(...validateField(item, field.items, `${path}[${index}]`));
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case ConfigType.OBJECT:
|
|
146
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
147
|
+
errors.push({ path, message: `"${path}" must be an object, got ${Array.isArray(value) ? 'array' : typeof value}`, value, rule: 'type' });
|
|
148
|
+
return errors;
|
|
149
|
+
}
|
|
150
|
+
// Validate nested properties
|
|
151
|
+
if (field.properties) {
|
|
152
|
+
for (const [key, propSchema] of Object.entries(field.properties)) {
|
|
153
|
+
errors.push(...validateField(value[key], propSchema, `${path}.${key}`));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Custom validation
|
|
160
|
+
if (field.validate && typeof field.validate === 'function') {
|
|
161
|
+
const customError = field.validate(value);
|
|
162
|
+
if (customError) {
|
|
163
|
+
errors.push({ path, message: customError, value, rule: 'custom' });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return errors;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Apply default values to a config object based on schema
|
|
172
|
+
*
|
|
173
|
+
* @param {object} config - The config object (will be mutated)
|
|
174
|
+
* @param {object} schema - Schema definition with properties
|
|
175
|
+
* @returns {object} The config with defaults applied
|
|
176
|
+
*/
|
|
177
|
+
function applyDefaults(config, schema) {
|
|
178
|
+
if (!schema || !schema.properties) return config;
|
|
179
|
+
|
|
180
|
+
for (const [key, field] of Object.entries(schema.properties)) {
|
|
181
|
+
if (config[key] === undefined && field.default !== undefined) {
|
|
182
|
+
config[key] = structuredClone(field.default);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Apply env var override
|
|
186
|
+
if (field.envVar && process.env[field.envVar] !== undefined) {
|
|
187
|
+
const envValue = process.env[field.envVar];
|
|
188
|
+
switch (field.type) {
|
|
189
|
+
case ConfigType.NUMBER:
|
|
190
|
+
config[key] = Number(envValue);
|
|
191
|
+
break;
|
|
192
|
+
case ConfigType.BOOLEAN:
|
|
193
|
+
config[key] = envValue === 'true' || envValue === '1';
|
|
194
|
+
break;
|
|
195
|
+
default:
|
|
196
|
+
config[key] = envValue;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Recurse into nested objects
|
|
201
|
+
if (field.type === ConfigType.OBJECT && field.properties && config[key]) {
|
|
202
|
+
applyDefaults(config[key], field);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return config;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Config Validator - Schema-based configuration validation
|
|
211
|
+
*/
|
|
212
|
+
export class ConfigValidator {
|
|
213
|
+
/**
|
|
214
|
+
* @param {object} schema - Top-level schema definition
|
|
215
|
+
* @param {object} schema.properties - Map of field names to SchemaField definitions
|
|
216
|
+
*/
|
|
217
|
+
constructor(schema) {
|
|
218
|
+
this.schema = schema;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Validate a config object against the schema
|
|
223
|
+
*
|
|
224
|
+
* @param {object} config
|
|
225
|
+
* @returns {{ valid: boolean, errors: ValidationError[] }}
|
|
226
|
+
*/
|
|
227
|
+
validate(config) {
|
|
228
|
+
if (!config || typeof config !== 'object') {
|
|
229
|
+
return {
|
|
230
|
+
valid: false,
|
|
231
|
+
errors: [{ path: '', message: 'Config must be a non-null object', value: config, rule: 'type' }],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const errors = [];
|
|
236
|
+
if (this.schema.properties) {
|
|
237
|
+
for (const [key, field] of Object.entries(this.schema.properties)) {
|
|
238
|
+
errors.push(...validateField(config[key], field, key));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { valid: errors.length === 0, errors };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Validate and apply defaults (non-destructive - returns a new object)
|
|
247
|
+
*
|
|
248
|
+
* @param {object} config
|
|
249
|
+
* @returns {{ valid: boolean, config: object, errors: ValidationError[] }}
|
|
250
|
+
*/
|
|
251
|
+
validateAndApplyDefaults(config) {
|
|
252
|
+
const merged = structuredClone(config || {});
|
|
253
|
+
applyDefaults(merged, this.schema);
|
|
254
|
+
|
|
255
|
+
const { valid, errors } = this.validate(merged);
|
|
256
|
+
return { valid, config: merged, errors };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Apply a runtime config patch with validation
|
|
261
|
+
*
|
|
262
|
+
* Distilled from OpenClaw's merge-patch.ts pattern — prevents prototype pollution
|
|
263
|
+
* and validates the resulting config.
|
|
264
|
+
*
|
|
265
|
+
* @param {object} baseConfig - Current config
|
|
266
|
+
* @param {object} patch - Partial config to merge
|
|
267
|
+
* @returns {{ valid: boolean, config: object, errors: ValidationError[], changes: Array }}
|
|
268
|
+
*/
|
|
269
|
+
applyPatch(baseConfig, patch) {
|
|
270
|
+
if (!patch || typeof patch !== 'object') {
|
|
271
|
+
return {
|
|
272
|
+
valid: true,
|
|
273
|
+
config: structuredClone(baseConfig),
|
|
274
|
+
errors: [],
|
|
275
|
+
changes: [],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const merged = structuredClone(baseConfig || {});
|
|
280
|
+
const changes = [];
|
|
281
|
+
|
|
282
|
+
// Safe merge (prototype pollution prevention)
|
|
283
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
284
|
+
// Block prototype pollution
|
|
285
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const oldValue = merged[key];
|
|
290
|
+
if (value === null) {
|
|
291
|
+
// null means "delete this key"
|
|
292
|
+
if (merged[key] !== undefined) {
|
|
293
|
+
changes.push({ path: key, action: 'delete', oldValue: merged[key] });
|
|
294
|
+
delete merged[key];
|
|
295
|
+
}
|
|
296
|
+
} else if (typeof value === 'object' && !Array.isArray(value) && typeof oldValue === 'object' && !Array.isArray(oldValue)) {
|
|
297
|
+
// Deep merge for nested objects
|
|
298
|
+
const nested = this._deepMergeSafe(oldValue, value, key);
|
|
299
|
+
merged[key] = nested.result;
|
|
300
|
+
changes.push(...nested.changes);
|
|
301
|
+
} else {
|
|
302
|
+
// Direct replacement
|
|
303
|
+
merged[key] = structuredClone(value);
|
|
304
|
+
if (JSON.stringify(oldValue) !== JSON.stringify(value)) {
|
|
305
|
+
changes.push({ path: key, action: oldValue === undefined ? 'add' : 'update', oldValue, newValue: value });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const { valid, errors } = this.validate(merged);
|
|
311
|
+
return { valid, config: merged, errors, changes };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Deep merge with prototype pollution protection
|
|
316
|
+
* @private
|
|
317
|
+
*/
|
|
318
|
+
_deepMergeSafe(target, source, parentPath) {
|
|
319
|
+
const result = { ...target };
|
|
320
|
+
const changes = [];
|
|
321
|
+
|
|
322
|
+
for (const [key, value] of Object.entries(source)) {
|
|
323
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const fullPath = `${parentPath}.${key}`;
|
|
328
|
+
const oldValue = result[key];
|
|
329
|
+
|
|
330
|
+
if (value === null) {
|
|
331
|
+
if (result[key] !== undefined) {
|
|
332
|
+
changes.push({ path: fullPath, action: 'delete', oldValue: result[key] });
|
|
333
|
+
delete result[key];
|
|
334
|
+
}
|
|
335
|
+
} else if (typeof value === 'object' && !Array.isArray(value) && typeof oldValue === 'object' && !Array.isArray(oldValue)) {
|
|
336
|
+
const nested = this._deepMergeSafe(oldValue, value, fullPath);
|
|
337
|
+
result[key] = nested.result;
|
|
338
|
+
changes.push(...nested.changes);
|
|
339
|
+
} else {
|
|
340
|
+
result[key] = structuredClone(value);
|
|
341
|
+
if (JSON.stringify(oldValue) !== JSON.stringify(value)) {
|
|
342
|
+
changes.push({ path: fullPath, action: oldValue === undefined ? 'add' : 'update', oldValue, newValue: value });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return { result, changes };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Diff two configs to find changes
|
|
352
|
+
*
|
|
353
|
+
* @param {object} oldConfig
|
|
354
|
+
* @param {object} newConfig
|
|
355
|
+
* @returns {Array<{path: string, action: string, oldValue: *, newValue: *}>}
|
|
356
|
+
*/
|
|
357
|
+
diff(oldConfig, newConfig) {
|
|
358
|
+
return this._diffObjects(oldConfig || {}, newConfig || {}, '');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* @private
|
|
363
|
+
*/
|
|
364
|
+
_diffObjects(oldObj, newObj, prefix) {
|
|
365
|
+
const changes = [];
|
|
366
|
+
const allKeys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
|
|
367
|
+
|
|
368
|
+
for (const key of allKeys) {
|
|
369
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
370
|
+
const oldVal = oldObj[key];
|
|
371
|
+
const newVal = newObj[key];
|
|
372
|
+
|
|
373
|
+
if (oldVal === undefined && newVal !== undefined) {
|
|
374
|
+
changes.push({ path, action: 'add', oldValue: undefined, newValue: newVal });
|
|
375
|
+
} else if (oldVal !== undefined && newVal === undefined) {
|
|
376
|
+
changes.push({ path, action: 'delete', oldValue: oldVal, newValue: undefined });
|
|
377
|
+
} else if (
|
|
378
|
+
typeof oldVal === 'object' && oldVal !== null && !Array.isArray(oldVal) &&
|
|
379
|
+
typeof newVal === 'object' && newVal !== null && !Array.isArray(newVal)
|
|
380
|
+
) {
|
|
381
|
+
changes.push(...this._diffObjects(oldVal, newVal, path));
|
|
382
|
+
} else if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
|
383
|
+
changes.push({ path, action: 'update', oldValue: oldVal, newValue: newVal });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return changes;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ============================================================================
|
|
392
|
+
// Pre-built schema for the enterprise application config
|
|
393
|
+
// ============================================================================
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Enterprise application config schema
|
|
397
|
+
*/
|
|
398
|
+
export const enterpriseConfigSchema = {
|
|
399
|
+
properties: {
|
|
400
|
+
company: {
|
|
401
|
+
type: ConfigType.OBJECT,
|
|
402
|
+
description: 'Company configuration',
|
|
403
|
+
required: true,
|
|
404
|
+
properties: {
|
|
405
|
+
name: {
|
|
406
|
+
type: ConfigType.STRING,
|
|
407
|
+
description: 'Company name',
|
|
408
|
+
required: true,
|
|
409
|
+
min: 1,
|
|
410
|
+
max: 100,
|
|
411
|
+
},
|
|
412
|
+
mission: {
|
|
413
|
+
type: ConfigType.STRING,
|
|
414
|
+
description: 'Company mission statement',
|
|
415
|
+
default: '',
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
providers: {
|
|
420
|
+
type: ConfigType.ARRAY,
|
|
421
|
+
description: 'LLM provider configurations',
|
|
422
|
+
default: [],
|
|
423
|
+
items: {
|
|
424
|
+
type: ConfigType.OBJECT,
|
|
425
|
+
properties: {
|
|
426
|
+
id: { type: ConfigType.STRING, required: true },
|
|
427
|
+
provider: {
|
|
428
|
+
type: ConfigType.ENUM,
|
|
429
|
+
enum: ['openai', 'anthropic', 'google', 'deepseek', 'qwen', 'zhipu', 'moonshot', 'custom'],
|
|
430
|
+
required: true,
|
|
431
|
+
},
|
|
432
|
+
apiKey: { type: ConfigType.STRING, envVar: 'LLM_API_KEY' },
|
|
433
|
+
model: { type: ConfigType.STRING, default: 'gpt-4o-mini' },
|
|
434
|
+
baseURL: { type: ConfigType.STRING },
|
|
435
|
+
maxTokens: { type: ConfigType.NUMBER, min: 1, max: 200000, default: 4096 },
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
session: {
|
|
440
|
+
type: ConfigType.OBJECT,
|
|
441
|
+
description: 'Session management settings',
|
|
442
|
+
default: {},
|
|
443
|
+
properties: {
|
|
444
|
+
maxSessions: { type: ConfigType.NUMBER, min: 1, max: 10000, default: 500 },
|
|
445
|
+
sessionTTL: { type: ConfigType.NUMBER, min: 0, default: 0 },
|
|
446
|
+
idleTimeout: { type: ConfigType.NUMBER, min: 0, default: 1800000 },
|
|
447
|
+
maxTranscriptLength: { type: ConfigType.NUMBER, min: 10, max: 1000, default: 100 },
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
security: {
|
|
451
|
+
type: ConfigType.OBJECT,
|
|
452
|
+
description: 'Security settings',
|
|
453
|
+
default: {},
|
|
454
|
+
properties: {
|
|
455
|
+
blockDangerousCommands: { type: ConfigType.BOOLEAN, default: true },
|
|
456
|
+
scanSecrets: { type: ConfigType.BOOLEAN, default: true },
|
|
457
|
+
maxFileSize: { type: ConfigType.NUMBER, min: 1024, default: 1048576 },
|
|
458
|
+
auditLogDir: { type: ConfigType.STRING, default: 'data/audit' },
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
cron: {
|
|
462
|
+
type: ConfigType.OBJECT,
|
|
463
|
+
description: 'Scheduler settings',
|
|
464
|
+
default: {},
|
|
465
|
+
properties: {
|
|
466
|
+
enabled: { type: ConfigType.BOOLEAN, default: false },
|
|
467
|
+
tickInterval: { type: ConfigType.NUMBER, min: 1000, default: 60000 },
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
hooks: {
|
|
471
|
+
type: ConfigType.OBJECT,
|
|
472
|
+
description: 'Hook system settings',
|
|
473
|
+
default: {},
|
|
474
|
+
properties: {
|
|
475
|
+
enabled: { type: ConfigType.BOOLEAN, default: true },
|
|
476
|
+
handlerTimeout: { type: ConfigType.NUMBER, min: 1000, max: 60000, default: 10000 },
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
// Global singleton with enterprise schema
|
|
483
|
+
export const configValidator = new ConfigValidator(enterpriseConfigSchema);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Internationalized time formatting utility
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function formatTimeI18n(time, t) {
|
|
7
|
+
if (!time) return '';
|
|
8
|
+
const d = new Date(time);
|
|
9
|
+
const now = new Date();
|
|
10
|
+
const diff = now - d;
|
|
11
|
+
|
|
12
|
+
if (diff < 60 * 1000) return t('time.justNow');
|
|
13
|
+
if (diff < 60 * 60 * 1000) return t('time.minutesAgo', { n: Math.floor(diff / 60000) });
|
|
14
|
+
if (diff < 24 * 60 * 60 * 1000) {
|
|
15
|
+
return d.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit' });
|
|
16
|
+
}
|
|
17
|
+
if (diff < 7 * 24 * 60 * 60 * 1000) {
|
|
18
|
+
const days = [t('time.sun'), t('time.mon'), t('time.tue'), t('time.wed'), t('time.thu'), t('time.fri'), t('time.sat')];
|
|
19
|
+
return days[d.getDay()];
|
|
20
|
+
}
|
|
21
|
+
return d.toLocaleDateString('en', { month: 'short', day: 'numeric' });
|
|
22
|
+
}
|