create-openclaw-bot 5.8.17 → 5.8.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/dist/server/local-server.js +91 -67
- package/dist/setup/shared/workspace-gen.js +16 -821
- package/dist/web/app.js +24 -4
- package/package.json +1 -1
|
@@ -88,6 +88,7 @@ const state = {
|
|
|
88
88
|
mode: null,
|
|
89
89
|
os: null,
|
|
90
90
|
startedAt: null,
|
|
91
|
+
projects: null,
|
|
91
92
|
};
|
|
92
93
|
|
|
93
94
|
function sendLog(line) {
|
|
@@ -1074,7 +1075,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
|
|
|
1074
1075
|
const emoji = String(body.emoji || '').trim();
|
|
1075
1076
|
const userName = String(body.userName || '').trim();
|
|
1076
1077
|
const userDesc = String(body.userDescription || body.userDesc || '').trim();
|
|
1077
|
-
const userInfo = [userName ? `- **
|
|
1078
|
+
const userInfo = [userName ? `- **Tên:** ${userName}` : '', userDesc ? `- **Mô tả:** ${userDesc}` : ''].filter(Boolean).join('\n');
|
|
1078
1079
|
|
|
1079
1080
|
const openclawHome = join(projectDir, '.openclaw');
|
|
1080
1081
|
await fsp.mkdir(openclawHome, { recursive: true });
|
|
@@ -1944,6 +1945,7 @@ async function saveState(rootProjectDir) {
|
|
|
1944
1945
|
gatewayPort: state.gatewayPort,
|
|
1945
1946
|
routerUrl: state.routerUrl,
|
|
1946
1947
|
routerPort: state.routerPort,
|
|
1948
|
+
projects: state.projects || [],
|
|
1947
1949
|
}, null, 2), 'utf8').catch(() => {});
|
|
1948
1950
|
}
|
|
1949
1951
|
|
|
@@ -1951,6 +1953,9 @@ async function loadSavedState(rootProjectDir) {
|
|
|
1951
1953
|
const file = join(rootProjectDir, STATE_FILE);
|
|
1952
1954
|
if (!existsSync(file)) return;
|
|
1953
1955
|
const saved = JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
1956
|
+
if (Array.isArray(saved?.projects)) {
|
|
1957
|
+
state.projects = saved.projects;
|
|
1958
|
+
}
|
|
1954
1959
|
if (saved?.projectDir && existsSync(join(saved.projectDir, '.openclaw', 'openclaw.json'))) {
|
|
1955
1960
|
Object.assign(state, saved, { installed: !!saved.installed });
|
|
1956
1961
|
await syncRuntimeState(state.projectDir);
|
|
@@ -1974,20 +1979,31 @@ function isRestrictedSystemDir(dirPath) {
|
|
|
1974
1979
|
if (lower.includes(':\\users\\') || lower.endsWith(':\\users')) {
|
|
1975
1980
|
const home = resolve(getRealHomedir()).toLowerCase();
|
|
1976
1981
|
if (lower !== home && !lower.startsWith(home + '\\') && !lower.startsWith(home + '/')) {
|
|
1977
|
-
|
|
1982
|
+
const cwd = resolve(process.cwd()).toLowerCase();
|
|
1983
|
+
const match = cwd.match(/^([a-z]:\\users\\[^\\]+)/) || cwd.match(/^(\/mnt\/[a-z]\/users\/[^\/]+)/);
|
|
1984
|
+
const cwdHome = match ? match[1] : '';
|
|
1985
|
+
if (!cwdHome || (lower !== cwdHome && !lower.startsWith(cwdHome + '\\') && !lower.startsWith(cwdHome + '/'))) {
|
|
1986
|
+
return true;
|
|
1987
|
+
}
|
|
1978
1988
|
}
|
|
1979
1989
|
}
|
|
1980
1990
|
|
|
1981
1991
|
if (process.platform !== 'win32') {
|
|
1982
1992
|
const unixBlacklist = new Set([
|
|
1983
|
-
'usr', 'var', 'proc', 'sys', 'dev', 'etc', 'sbin', 'bin', 'lib', 'lib64', 'run', 'tmp', 'boot', 'lost+found', 'srv', 'mnt', 'media', 'opt'
|
|
1993
|
+
'usr', 'var', 'proc', 'sys', 'dev', 'etc', 'sbin', 'bin', 'lib', 'lib64', 'run', 'tmp', 'boot', 'lost+found', 'srv', 'mnt', 'media', 'opt',
|
|
1994
|
+
'applications', 'library', 'system', 'volumes', 'private', 'cores', 'network', 'users'
|
|
1984
1995
|
]);
|
|
1985
1996
|
if (unixBlacklist.has(basename(lower))) return true;
|
|
1986
1997
|
|
|
1987
|
-
if (lower.startsWith('/home') || lower.startsWith('/root')) {
|
|
1998
|
+
if (lower.startsWith('/home') || lower.startsWith('/root') || lower.startsWith('/mnt/') || lower.startsWith('/users/') || lower === '/users') {
|
|
1988
1999
|
const realHome = resolve(getRealHomedir()).toLowerCase();
|
|
1989
2000
|
if (lower !== realHome && !lower.startsWith(realHome + '/')) {
|
|
1990
|
-
|
|
2001
|
+
const cwd = resolve(process.cwd()).toLowerCase();
|
|
2002
|
+
const match = cwd.match(/^(\/home\/[^\/]+)/) || cwd.match(/^(\/root)/) || cwd.match(/^(\/mnt\/[a-z]\/users\/[^\/]+)/) || cwd.match(/^(\/users\/[^\/]+)/);
|
|
2003
|
+
const cwdHome = match ? match[1] : '';
|
|
2004
|
+
if (!cwdHome || (lower !== cwdHome && !lower.startsWith(cwdHome + '/'))) {
|
|
2005
|
+
return true;
|
|
2006
|
+
}
|
|
1991
2007
|
}
|
|
1992
2008
|
}
|
|
1993
2009
|
}
|
|
@@ -2045,73 +2061,52 @@ async function findLatestProject(rootProjectDir) {
|
|
|
2045
2061
|
return candidates[0]?.dir || null;
|
|
2046
2062
|
}
|
|
2047
2063
|
|
|
2048
|
-
async function
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
].filter(Boolean);
|
|
2058
|
-
|
|
2059
|
-
const drives = await getAvailableDrives();
|
|
2060
|
-
for (const drive of drives) {
|
|
2061
|
-
const entries = await fsp.readdir(drive, { withFileTypes: true }).catch(() => []);
|
|
2062
|
-
for (const e of entries) {
|
|
2063
|
-
if (e.isDirectory() && !e.name.startsWith('$') && !SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) {
|
|
2064
|
-
const fullPath = join(drive, e.name);
|
|
2065
|
-
if (!isRestrictedSystemDir(fullPath)) {
|
|
2066
|
-
roots.push(fullPath);
|
|
2067
|
-
}
|
|
2064
|
+
async function ensureProjectsLoaded(rootProjectDir) {
|
|
2065
|
+
if (state.projects !== null) return;
|
|
2066
|
+
state.projects = [];
|
|
2067
|
+
const file = join(rootProjectDir, STATE_FILE);
|
|
2068
|
+
if (existsSync(file)) {
|
|
2069
|
+
try {
|
|
2070
|
+
const saved = JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
2071
|
+
if (Array.isArray(saved?.projects)) {
|
|
2072
|
+
state.projects = saved.projects;
|
|
2068
2073
|
}
|
|
2069
|
-
}
|
|
2074
|
+
} catch {}
|
|
2070
2075
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
const st = await fsp.stat(cfgPath).catch(() => null);
|
|
2082
|
-
const runtime = await detectRuntime(full).catch(() => ({ mode: 'unknown', gatewayPort: 0, routerPort: 0, syncSource: 'config' }));
|
|
2083
|
-
const bots = await listConfiguredBots(full).catch(() => []);
|
|
2084
|
-
const uniqueBotCount = new Set(bots.map((b) => b.id)).size;
|
|
2085
|
-
const hasDocker = existsSync(join(full, 'docker', 'openclaw', 'docker-compose.yml'));
|
|
2086
|
-
const isLikelyProject = uniqueBotCount > 0 || hasDocker || existsSync(join(full, '.env')) || existsSync(join(full, 'package.json'));
|
|
2087
|
-
if (!isLikelyProject) return;
|
|
2088
|
-
hits.push({
|
|
2089
|
-
projectDir: full,
|
|
2090
|
-
os: process.platform === 'win32' ? 'Windows' : process.platform === 'darwin' ? 'macOS' : 'Linux',
|
|
2091
|
-
mode: runtime.mode || 'unknown',
|
|
2092
|
-
gatewayPort: runtime.gatewayPort || 0,
|
|
2093
|
-
routerPort: runtime.routerPort || 0,
|
|
2094
|
-
syncSource: runtime.syncSource || 'config',
|
|
2095
|
-
botCount: uniqueBotCount,
|
|
2096
|
-
hasDocker,
|
|
2097
|
-
updatedAt: st?.mtimeMs || 0,
|
|
2098
|
-
});
|
|
2099
|
-
return;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
async function discoverProjects(rootProjectDir) {
|
|
2079
|
+
await ensureProjectsLoaded(rootProjectDir);
|
|
2080
|
+
|
|
2081
|
+
if (state.projectDir && existsSync(join(state.projectDir, '.openclaw', 'openclaw.json'))) {
|
|
2082
|
+
const resolved = resolve(state.projectDir);
|
|
2083
|
+
if (!state.projects.some(p => resolve(p.projectDir) === resolved)) {
|
|
2084
|
+
const meta = await buildProjectMeta(resolved).catch(() => null);
|
|
2085
|
+
if (meta) state.projects.push(meta);
|
|
2100
2086
|
}
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
const updatedProjects = [];
|
|
2090
|
+
for (const p of state.projects) {
|
|
2091
|
+
if (existsSync(join(p.projectDir, '.openclaw', 'openclaw.json'))) {
|
|
2092
|
+
const meta = await buildProjectMeta(p.projectDir).catch(() => null);
|
|
2093
|
+
if (meta) {
|
|
2094
|
+
updatedProjects.push(meta);
|
|
2095
|
+
}
|
|
2106
2096
|
}
|
|
2107
2097
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
(
|
|
2112
|
-
(
|
|
2113
|
-
|
|
2114
|
-
|
|
2098
|
+
state.projects = updatedProjects;
|
|
2099
|
+
|
|
2100
|
+
state.projects.sort((a, b) => {
|
|
2101
|
+
const aActive = state.projectDir && resolve(state.projectDir) === resolve(a.projectDir);
|
|
2102
|
+
const bActive = state.projectDir && resolve(state.projectDir) === resolve(b.projectDir);
|
|
2103
|
+
if (aActive && !bActive) return -1;
|
|
2104
|
+
if (!aActive && bActive) return 1;
|
|
2105
|
+
return (b.botCount - a.botCount) || (b.updatedAt - a.updatedAt);
|
|
2106
|
+
});
|
|
2107
|
+
|
|
2108
|
+
await saveState(rootProjectDir).catch(() => {});
|
|
2109
|
+
return state.projects.slice(0, 20);
|
|
2115
2110
|
}
|
|
2116
2111
|
|
|
2117
2112
|
async function resolveProjectDir(rootProjectDir, body = {}) {
|
|
@@ -2143,10 +2138,37 @@ async function resolveProjectDir(rootProjectDir, body = {}) {
|
|
|
2143
2138
|
return state.projectDir;
|
|
2144
2139
|
}
|
|
2145
2140
|
|
|
2141
|
+
async function buildProjectMeta(projectDir) {
|
|
2142
|
+
const full = resolve(projectDir);
|
|
2143
|
+
const cfgPath = join(full, '.openclaw', 'openclaw.json');
|
|
2144
|
+
const st = await fsp.stat(cfgPath).catch(() => null);
|
|
2145
|
+
const runtime = await detectRuntime(full).catch(() => ({ mode: 'unknown', gatewayPort: 0, routerPort: 0, syncSource: 'config' }));
|
|
2146
|
+
const bots = await listConfiguredBots(full).catch(() => []);
|
|
2147
|
+
const uniqueBotCount = new Set(bots.map((b) => b.id)).size;
|
|
2148
|
+
const hasDocker = existsSync(join(full, 'docker', 'openclaw', 'docker-compose.yml'));
|
|
2149
|
+
return {
|
|
2150
|
+
projectDir: full,
|
|
2151
|
+
os: process.platform === 'win32' ? 'Windows' : process.platform === 'darwin' ? 'macOS' : 'Linux',
|
|
2152
|
+
mode: runtime.mode || 'unknown',
|
|
2153
|
+
gatewayPort: runtime.gatewayPort || 0,
|
|
2154
|
+
routerPort: runtime.routerPort || 0,
|
|
2155
|
+
syncSource: runtime.syncSource || 'config',
|
|
2156
|
+
botCount: uniqueBotCount,
|
|
2157
|
+
hasDocker,
|
|
2158
|
+
updatedAt: st?.mtimeMs || 0,
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2146
2162
|
async function connectExistingProject(projectDir, rootProjectDir) {
|
|
2147
2163
|
const resolved = resolve(String(projectDir || ''));
|
|
2148
2164
|
if (!existsSync(join(resolved, '.openclaw', 'openclaw.json'))) throw httpError(404, 'openclaw.json not found in selected project');
|
|
2149
2165
|
await syncRuntimeState(resolved);
|
|
2166
|
+
await ensureProjectsLoaded(rootProjectDir);
|
|
2167
|
+
const meta = await buildProjectMeta(resolved).catch(() => null);
|
|
2168
|
+
if (meta) {
|
|
2169
|
+
state.projects = state.projects.filter(p => resolve(p.projectDir) !== resolved);
|
|
2170
|
+
state.projects.unshift(meta);
|
|
2171
|
+
}
|
|
2150
2172
|
await saveState(rootProjectDir);
|
|
2151
2173
|
const bots = await listConfiguredBots(resolved).catch(() => []);
|
|
2152
2174
|
return {
|
|
@@ -2200,6 +2222,8 @@ async function deleteProjectFolder(projectDir, rootProjectDir) {
|
|
|
2200
2222
|
state.projectDir = null;
|
|
2201
2223
|
state.installed = false;
|
|
2202
2224
|
}
|
|
2225
|
+
await ensureProjectsLoaded(rootProjectDir);
|
|
2226
|
+
state.projects = state.projects.filter(p => resolve(p.projectDir) !== resolved);
|
|
2203
2227
|
await saveState(rootProjectDir);
|
|
2204
2228
|
return { ok: true, projectDir: resolved };
|
|
2205
2229
|
}
|
|
@@ -387,827 +387,22 @@ description: Schedule recurring tasks using the cron tool.
|
|
|
387
387
|
- Skip internal doc lookups such as \`cron-jobs.mdx\`; rely on the available tools and complete the scheduling task directly.`;
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
function buildInfographicGeneratorSkillMd(botName = 'Williams') {
|
|
391
|
-
return
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
## 📐 2. QUY ĐỊNH KÍCH THƯỚC & TỶ LỆ (ASPECT RATIO)
|
|
408
|
-
|
|
409
|
-
Khi gọi API, mặc định kích thước là tỷ lệ **1:1** (hình vuông). Tuy nhiên, hãy tùy biến linh hoạt theo yêu cầu của người dùng bằng cách điều chỉnh từ khóa mô tả tỷ lệ và khung hình trong prompt:
|
|
410
|
-
|
|
411
|
-
- **Mặc định (1:1)**: Thêm từ khóa \`square aspect ratio, 1:1 square canvas\` vào prompt. Phù hợp cho các infographic dạng ô lưới hoặc bài đăng mạng xã hội thông thường.
|
|
412
|
-
- **Poster dọc (Vertical Poster)**: Thêm từ khóa \`vertical poster aspect ratio, 2:3 portrait format, vertical infographic\` vào prompt. Phù hợp cho cẩm nang chi tiết có nhiều mục (3-9 mục).
|
|
413
|
-
- **Landscape (16:9)**: Thêm từ khóa \`16:9 landscape aspect ratio, wide horizontal banner\` vào prompt. Phù hợp cho banner nằm ngang, ảnh bìa.
|
|
414
|
-
|
|
415
|
-
---
|
|
416
|
-
|
|
417
|
-
## ✍️ 3. QUY ĐỊNH FOOTER BẮT BUỘC
|
|
418
|
-
|
|
419
|
-
Mọi ảnh infographic/poster được tạo ra bằng skill này bắt buộc phải có dòng chữ bản quyền nằm ở cạnh dưới, canh giữa:
|
|
420
|
-
|
|
421
|
-
- **Nội dung chữ bắt buộc**: \`"designed by ${botName} - trợ lý của tuanminhhole"\`
|
|
422
|
-
- **Cách mô tả trong prompt**: Thêm vào cuối prompt mô tả chi tiết:
|
|
423
|
-
_\`"At the bottom center of the image, there is a clean and tiny centered footer text that reads: 'designed by ${botName} - trợ lý của tuanminhhole'"\`_
|
|
424
|
-
|
|
425
|
-
---
|
|
426
|
-
|
|
427
|
-
## 🎨 4. BA PHONG CÁCH THIẾT KẾ CHỦ ĐẠO
|
|
428
|
-
|
|
429
|
-
Hãy chọn 1 trong 3 phong cách dưới đây tùy thuộc vào ngữ cảnh yêu cầu:
|
|
430
|
-
|
|
431
|
-
### Phong cách 1: Tin tức báo chí / News Editorial
|
|
432
|
-
|
|
433
|
-
- **Đặc điểm**: Bố cục chuyên nghiệp, chia nhiều cột dọc/ngang (multi-column), sử dụng các đường kẻ mỏng hoặc nét đứt mảnh để phân chia các ô tin tức rõ ràng.
|
|
434
|
-
- **Phông chữ**: Font tiêu đề Serif (có chân) sang trọng, font nội dung Sans-serif (không chân) hiện đại.
|
|
435
|
-
- **Minh họa**: Icon dạng vector phẳng (flat vector icons), tối giản, chuyên nghiệp.
|
|
436
|
-
- **Từ khóa prompt gợi ý**: \`news editorial infographic style, newspaper grid layout, clear divider lines, minimal serif headers, flat vector icons, professional business theme, clean corporate colors.\`
|
|
437
|
-
|
|
438
|
-
### Phong cách 2: Cẩm nang/Hướng dẫn chi tiết
|
|
439
|
-
|
|
440
|
-
- **Đặc điểm**: Bố cục lưới (ví dụ: 3x3 grid) gồm nhiều ô được đánh số thứ tự (1, 2, 3...). Mỗi ô có nền màu pastel nhẹ nhàng (như xanh lá nhạt, kem nhạt, vàng nhạt) với viền bo góc tròn mềm mại. Có hình mascot (như chú heo đất đeo kính, két sắt, nhân vật hoạt hình) xuất hiện làm điểm nhấn.
|
|
441
|
-
- **Phông chữ**: Font chữ tròn, thân thiện, rõ ràng.
|
|
442
|
-
- **Minh họa**: Icon hoạt hình 2D sống động, nhiều màu sắc.
|
|
443
|
-
- **Từ khóa prompt gợi ý**: \`detailed guide infographic poster, 3x3 numbered grid layout, rounded pastel cards, cute 2D cartoon mascot, playful vector icons, warm cream background, clear numbered badges.\`
|
|
444
|
-
|
|
445
|
-
### Phong cách 3: Layout Neo-Brutalism hoạt hình
|
|
446
|
-
|
|
447
|
-
- **Đặc điểm**: Đường viền đen dày nổi bật (thick dark borders), đổ bóng cứng màu đen (hard solid drop shadows), màu sắc tương phản mạnh mẽ (Neo-Brutalism), phong cách hoạt hình 2D phẳng, hiện đại và trẻ trung.
|
|
448
|
-
- **Phông chữ**: Font chữ in đậm, cá tính và không chân.
|
|
449
|
-
- **Minh họa**: Mascot và các icon phẳng nét vẽ dày cá tính.
|
|
450
|
-
- **Từ khóa prompt gợi ý**: \`neo-brutalism infographic poster, vector cartoon flat 2D style, thick dark solid borders, hard black drop shadows, bright vibrant background cards (yellow, cyan, lime green, orange), playful modern bold typography.\`
|
|
451
|
-
|
|
452
|
-
---
|
|
453
|
-
|
|
454
|
-
## 🔤 5. QUY TẮC PHÒNG TRÁNH LỖI FONT TIẾNG VIỆT
|
|
455
|
-
|
|
456
|
-
Mô hình Gemini 3.1 Flash Image hỗ trợ ghi text tiếng Việt cực tốt, nhưng để tránh việc AI tự động dùng các font chữ lạ bị lỗi hiển thị dấu tiếng Việt (như phác, ngã, hỏi bị lệch phông), hãy áp dụng nghiêm ngặt các quy tắc sau:
|
|
457
|
-
|
|
458
|
-
1. **Chỉ định phông chữ tiêu chuẩn**: Trong prompt, ghi rõ tên các font chữ phổ biến hỗ trợ Unicode tiếng Việt tốt như: **Arial, Inter, Montserrat, Roboto, Plus Jakarta Sans, Fredoka** (chỉ dùng cho phong cách hoạt hình).
|
|
459
|
-
_Ví dụ: "in clean bold Arial font", "using modern Montserrat typeface"._
|
|
460
|
-
2. **Tránh phông chữ lạ**: Tuyệt đối **KHÔNG** sử dụng các từ khóa như \`decorative, script, handwritten, gothic, calligraphy, futuristic fonts\` vì chúng hầu như không hỗ trợ tiếng Việt và sẽ tạo ra chữ lỗi phông rất xấu.
|
|
461
|
-
3. **Định dạng Text rõ ràng**: Đặt toàn bộ các đoạn text tiếng Việt cần hiển thị trong dấu nháy đơn hoặc nháy kép để mô hình nhận diện chính xác phần văn bản cần viết.
|
|
462
|
-
_Ví dụ: \`At the top, the main title in bold Arial font reads: 'BÍ KÍP TRÁNH NÓNG MÙA HÈ'\`._
|
|
463
|
-
|
|
464
|
-
---
|
|
465
|
-
|
|
466
|
-
## 📝 6. MẪU PROMPT CHUNG CHO BOT LLM (TÙY CHỈNH THEO YÊU CẦU)
|
|
467
|
-
|
|
468
|
-
Mẫu prompt này được đúc kết từ các prompt tiêu chuẩn giúp mô hình tạo ảnh hoạt động tối ưu nhất. Bot LLM sẽ tự động tùy biến các phần nằm trong dấu ngoặc vuông \`[...]\` dựa trên tiêu đề, nội dung, số lượng bố cục và màu sắc phù hợp với chủ đề của người dùng, trong khi các phần còn lại được giữ nguyên cố định (bao gồm phong cách vẽ và footer bản quyền).
|
|
469
|
-
|
|
470
|
-
### A. Công thức Prompt Tiếng Anh (Khuyên Dùng cho API)
|
|
471
|
-
|
|
472
|
-
\`\`\`text
|
|
473
|
-
An infographic poster with [Tỷ lệ khung hình] and [Loại nền].
|
|
474
|
-
Art style is modern illustration style mixed with hand-drawn elements.
|
|
475
|
-
At the top, the main title in clean bold [Tên Font tiếng Việt chuẩn] reads: '[TIÊU ĐỀ TIẾNG VIỆT LỚN]'.
|
|
476
|
-
The layout is divided into [Số lượng] cards or sections [Bố cục chia ô từ trên xuống dưới / Bố cục ô lưới / Quy trình cách thức].
|
|
477
|
-
The background and accent colors of the cards are [Màu sắc hài hòa tương ứng phù hợp với chủ đề].
|
|
478
|
-
Each card contains a clean flat vector illustration representing [Mô tả ngắn gọn hình vẽ minh họa] and a clear text label in bold [Tên Font tiếng Việt chuẩn] reads: '[NHÃN TIẾNG VIỆT CHO TỪNG Ô]'.
|
|
479
|
-
The text throughout the image must be clean, legible, and easy to read.
|
|
480
|
-
At the bottom center of the image, there is a clean and tiny centered footer text that reads: 'designed by ${botName} - trợ lý của tuanminhhole'.
|
|
481
|
-
High-resolution, high quality, professional infographic poster, no spelling mistakes.
|
|
482
|
-
\`\`\`
|
|
483
|
-
|
|
484
|
-
### B. Công thức Prompt Tiếng Việt (Phong cách gốc giống Ảnh mẫu)
|
|
485
|
-
|
|
486
|
-
\`\`\`text
|
|
487
|
-
Infographic [Khung hình/tỷ lệ], nền [Loại nền].
|
|
488
|
-
Phong cách minh họa hiện đại pha hand-drawn.
|
|
489
|
-
Tiêu đề lớn '[TIÊU ĐỀ TIẾNG VIỆT LỚN]'.
|
|
490
|
-
Bố cục chia [Số lượng] ô rõ ràng [từ trên xuống dưới / dạng lưới / quy trình cách thức].
|
|
491
|
-
Màu sắc hài hòa [Mô tả tông màu phù hợp].
|
|
492
|
-
Mỗi ô vẽ minh họa vector phẳng [Mô tả ngắn hình ảnh cần vẽ cho ô] và nhãn chữ '[NHÃN TIẾNG VIỆT]'.
|
|
493
|
-
Chữ rõ ràng, dễ đọc, không sai chính tả.
|
|
494
|
-
Cạnh dưới canh giữa có chữ nhỏ: 'designed by ${botName} - trợ lý của tuanminhhole'.
|
|
495
|
-
Ảnh chất lượng cao, sắc nét.
|
|
496
|
-
\`\`\`
|
|
497
|
-
|
|
498
|
-
## Related
|
|
499
|
-
- [Hành động](../../TOOLS.md)`;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function buildInfographicGeneratorJs() {
|
|
503
|
-
return `const fs = require('fs');
|
|
504
|
-
const path = require('path');
|
|
505
|
-
|
|
506
|
-
const prompt = process.argv[2];
|
|
507
|
-
const outputPath = process.argv[3] || 'image.png';
|
|
508
|
-
|
|
509
|
-
if (!prompt) {
|
|
510
|
-
console.error('Usage: node image-generator.js "<prompt>" [output_path]');
|
|
511
|
-
process.exit(1);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Find openclaw.json path dynamically by walking up
|
|
515
|
-
let openclawJsonPath = '';
|
|
516
|
-
let currentDir = process.cwd();
|
|
517
|
-
for (let i = 0; i < 5; i++) {
|
|
518
|
-
const candidate = path.join(currentDir, 'openclaw.json');
|
|
519
|
-
if (fs.existsSync(candidate)) {
|
|
520
|
-
openclawJsonPath = candidate;
|
|
521
|
-
break;
|
|
522
|
-
}
|
|
523
|
-
const candidateInDot = path.join(currentDir, '.openclaw', 'openclaw.json');
|
|
524
|
-
if (fs.existsSync(candidateInDot)) {
|
|
525
|
-
openclawJsonPath = candidateInDot;
|
|
526
|
-
break;
|
|
527
|
-
}
|
|
528
|
-
const parent = path.dirname(currentDir);
|
|
529
|
-
if (parent === currentDir) break;
|
|
530
|
-
currentDir = parent;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Resolve API Key and Base URL from openclaw.json
|
|
534
|
-
let apiKey = process.env.NINE_ROUTER_API_KEY || ''; // default fallback key
|
|
535
|
-
let baseUrl = process.env.NINE_ROUTER_BASE_URL || 'http://9router:20128/v1'; // default fallback URL
|
|
536
|
-
if (openclawJsonPath) {
|
|
537
|
-
try {
|
|
538
|
-
const config = JSON.parse(fs.readFileSync(openclawJsonPath, 'utf8'));
|
|
539
|
-
const provider = config.models?.providers?.['9router'];
|
|
540
|
-
if (provider) {
|
|
541
|
-
if (provider.apiKey) apiKey = provider.apiKey;
|
|
542
|
-
if (provider.baseUrl) baseUrl = provider.baseUrl;
|
|
543
|
-
}
|
|
544
|
-
} catch (e) {}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const modelPriorityPatterns = [
|
|
548
|
-
/recraft-?v3/i,
|
|
549
|
-
/flux-pro-?(v1\\.1-)?ultra/i,
|
|
550
|
-
/flux-kontext-max/i,
|
|
551
|
-
/flux-pro-?(v1\\.1)?/i,
|
|
552
|
-
/flux-kontext-pro/i,
|
|
553
|
-
/recraft-?v2/i,
|
|
554
|
-
/recraft/i,
|
|
555
|
-
/ideogram-?v2/i,
|
|
556
|
-
/ideogram/i,
|
|
557
|
-
/runway.*turbo/i,
|
|
558
|
-
/runway/i,
|
|
559
|
-
/flux-?(1-)?dev/i,
|
|
560
|
-
/dall-e-3/i,
|
|
561
|
-
/stable-image-ultra/i,
|
|
562
|
-
/sd3\\.5-large-turbo/i,
|
|
563
|
-
/sd3\\.5-large/i,
|
|
564
|
-
/stable-diffusion-v35/i,
|
|
565
|
-
/sd3\\.5/i,
|
|
566
|
-
/stable-image-core/i,
|
|
567
|
-
/stable-diffusion-3/i,
|
|
568
|
-
/sd3/i,
|
|
569
|
-
/sd3\\.5-medium/i,
|
|
570
|
-
/flux-?(1-)?schnell/i,
|
|
571
|
-
/grok/i,
|
|
572
|
-
/gpt/i,
|
|
573
|
-
/minimax/i,
|
|
574
|
-
/gemini-3\\.1/i,
|
|
575
|
-
/gemini-3/i,
|
|
576
|
-
/gemini-2\\.5/i,
|
|
577
|
-
/gemini/i,
|
|
578
|
-
/sdxl/i,
|
|
579
|
-
/stable-diffusion/i,
|
|
580
|
-
/sdwebui/i,
|
|
581
|
-
/comfyui/i,
|
|
582
|
-
];
|
|
583
|
-
|
|
584
|
-
(async () => {
|
|
585
|
-
try {
|
|
586
|
-
// Query active image generation models to choose the best one
|
|
587
|
-
let selectedModel = '';
|
|
588
|
-
try {
|
|
589
|
-
const modelsResponse = await fetch(\`\${baseUrl}/models/image\`, {
|
|
590
|
-
headers: {
|
|
591
|
-
'Authorization': \`Bearer \${apiKey}\`
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
const modelsData = await modelsResponse.json();
|
|
595
|
-
if (modelsData && Array.isArray(modelsData.data) && modelsData.data.length > 0) {
|
|
596
|
-
const modelIds = modelsData.data.map(m => m.id);
|
|
597
|
-
for (const pattern of modelPriorityPatterns) {
|
|
598
|
-
const found = modelIds.find(id => pattern.test(id));
|
|
599
|
-
if (found) {
|
|
600
|
-
selectedModel = found;
|
|
601
|
-
break;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
if (!selectedModel) {
|
|
605
|
-
selectedModel = modelIds[0];
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
} catch (e) {
|
|
609
|
-
console.warn('[ImageGen] Failed to auto-resolve active models, using fallback:', e.message);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (!selectedModel) {
|
|
613
|
-
selectedModel = 'gemini/gemini-3.1-flash-image-preview'; // default fallback
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
console.log(\`[ImageGen] Generating: "\${prompt}" using model "\${selectedModel}"...\`);
|
|
617
|
-
const response = await fetch(\`\${baseUrl}/images/generations\`, {
|
|
618
|
-
method: 'POST',
|
|
619
|
-
headers: {
|
|
620
|
-
'Content-Type': 'application/json',
|
|
621
|
-
'Authorization': \`Bearer \${apiKey}\`
|
|
622
|
-
},
|
|
623
|
-
body: JSON.stringify({
|
|
624
|
-
model: selectedModel,
|
|
625
|
-
prompt: prompt,
|
|
626
|
-
n: 1,
|
|
627
|
-
size: 'auto',
|
|
628
|
-
response_format: 'b64_json'
|
|
629
|
-
})
|
|
630
|
-
});
|
|
631
|
-
const data = await response.json();
|
|
632
|
-
if (data.error) {
|
|
633
|
-
console.error('[ImageGen] API Error:', data.error.message || data.error);
|
|
634
|
-
process.exit(1);
|
|
635
|
-
}
|
|
636
|
-
if (data.data && data.data[0] && data.data[0].b64_json) {
|
|
637
|
-
const buf = Buffer.from(data.data[0].b64_json, 'base64');
|
|
638
|
-
const absoluteOutputPath = path.isAbsolute(outputPath) ? outputPath : path.join(process.cwd(), outputPath);
|
|
639
|
-
fs.writeFileSync(absoluteOutputPath, buf);
|
|
640
|
-
console.log(\`[ImageGen] Saved image to: \${outputPath}\`);
|
|
641
|
-
} else {
|
|
642
|
-
console.error('[ImageGen] No image data returned');
|
|
643
|
-
process.exit(1);
|
|
644
|
-
}
|
|
645
|
-
} catch (e) {
|
|
646
|
-
console.error('[ImageGen] Fetch Error:', e.message);
|
|
647
|
-
process.exit(1);
|
|
648
|
-
}
|
|
649
|
-
})();
|
|
650
|
-
`;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
function buildStickerMentionSkillMd(botName = 'Williams') {
|
|
654
|
-
return `---
|
|
655
|
-
name: sticker-mention
|
|
656
|
-
description: Tự động tag người gửi trong group chat và gửi sticker Zalo theo từ khóa.
|
|
657
|
-
---
|
|
658
|
-
|
|
659
|
-
## Tự động Tag và gửi sticker
|
|
660
|
-
|
|
661
|
-
- **Trong group chat (Tự động Tag)**: Hệ thống đã cài đặt cơ chế tự động tag người gửi tin nhắn gần nhất bằng cách chèn \`@Tên\` ở đầu câu trả lời. ${botName} không cần tự chèn \`@Tên\` của người gửi ở đầu câu nữa (hệ thống sẽ tự động làm). Nhưng nếu muốn nhắc đến một người khác hoặc tag ở giữa câu, hãy viết \`@TênHiểnThị\`.
|
|
662
|
-
|
|
663
|
-
## 🎭 Rules sử dụng Sticker Zalo
|
|
664
|
-
|
|
665
|
-
Để câu trả lời thêm phần sinh động và cà khịa tự nhiên, ${botName} có thể gửi kèm Sticker bằng cách viết mã \`[Sticker: <từ_khóa>]\` ở **CUỐI** tin nhắn.
|
|
666
|
-
|
|
667
|
-
Cơ chế hoạt động: Zalo sẽ tự động tìm kiếm sticker theo \`<từ_khóa>\` và gửi đi. Hãy sử dụng từ khóa ngắn gọn, rõ ràng bằng tiếng Việt hoặc tiếng Anh phù hợp với ngữ cảnh và cảm xúc hiện tại.
|
|
668
|
-
|
|
669
|
-
Ví dụ các từ khóa gợi ý:
|
|
670
|
-
|
|
671
|
-
- \`love\` hoặc \`ôm tim\` (khi cảm ơn, bắn tim, thể hiện tình cảm)
|
|
672
|
-
- \`ca khia\` hoặc \`leu leu\` (khi cà khịa, lêu lêu trêu chọc user)
|
|
673
|
-
- \`haha\` (khi cười vui vẻ, đập bàn cười)
|
|
674
|
-
- \`khóc\` hoặc \`sad\` (khi buồn bã, khóc ròng, tội nghiệp)
|
|
675
|
-
- \`tuc gian\` hoặc \`angry\` (khi tức giận, bị trêu chọc)
|
|
676
|
-
- \`thank you\` (khi cảm ơn hoặc chào tạm biệt)
|
|
677
|
-
- \`hi\` hoặc \`chào\` (khi bắt đầu trò chuyện)
|
|
678
|
-
|
|
679
|
-
_Lưu ý: Chỉ chèn tối đa 1 Sticker ở cuối tin nhắn khi thực sự phù hợp với ngữ cảnh (ví dụ chào hỏi, lêu lêu, khóc lóc hoặc cà khịa). Không lạm dụng._
|
|
680
|
-
|
|
681
|
-
## Related
|
|
682
|
-
- [Hành động](../../TOOLS.md)`;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function buildStickerMentionJs() {
|
|
686
|
-
return `/**
|
|
687
|
-
* Patch @openclaw/zalouser to support outgoing @mentions and stickers in group messages.
|
|
688
|
-
*
|
|
689
|
-
* Run: docker exec openclaw-williams node /mnt/project/patch-mentions.js
|
|
690
|
-
*/
|
|
691
|
-
const fs = require('fs');
|
|
692
|
-
const path = require('path');
|
|
693
|
-
|
|
694
|
-
const searchDirs = [
|
|
695
|
-
'/root/project/.openclaw/extensions/zalouser/dist',
|
|
696
|
-
'/root/project/.openclaw/npm/node_modules/@openclaw/zalouser/dist',
|
|
697
|
-
'/home/node/project/.openclaw/extensions/zalouser/dist',
|
|
698
|
-
'/home/node/project/.openclaw/npm/node_modules/@openclaw/zalouser/dist'
|
|
699
|
-
];
|
|
700
|
-
|
|
701
|
-
// Scan dynamic projects inside npm/projects/
|
|
702
|
-
const projectBases = ['/root/project/.openclaw', '/home/node/project/.openclaw'];
|
|
703
|
-
for (const base of projectBases) {
|
|
704
|
-
const projectsDir = path.join(base, 'npm', 'projects');
|
|
705
|
-
if (fs.existsSync(projectsDir)) {
|
|
706
|
-
try {
|
|
707
|
-
const dirs = fs.readdirSync(projectsDir);
|
|
708
|
-
for (const projectDir of dirs) {
|
|
709
|
-
const candidateDist = path.join(projectsDir, projectDir, 'node_modules', '@openclaw', 'zalouser', 'dist');
|
|
710
|
-
if (fs.existsSync(candidateDist)) {
|
|
711
|
-
searchDirs.push(candidateDist);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
} catch (_) {}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
const filesToPatch = [];
|
|
719
|
-
for (const dir of searchDirs) {
|
|
720
|
-
if (fs.existsSync(dir)) {
|
|
721
|
-
const files = fs.readdirSync(dir);
|
|
722
|
-
const found = files.find(f => f.startsWith('zalo-js-') && f.endsWith('.js') && !f.endsWith('.bak'));
|
|
723
|
-
if (found) {
|
|
724
|
-
filesToPatch.push(path.join(dir, found));
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
if (filesToPatch.length === 0) {
|
|
730
|
-
console.error('[patch-mentions] Error: Zalo JS files not found.');
|
|
731
|
-
process.exit(1);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
if (process.argv.includes('--restore')) {
|
|
735
|
-
let restoredCount = 0;
|
|
736
|
-
for (const FILE of filesToPatch) {
|
|
737
|
-
const BACKUP = FILE + '.bak';
|
|
738
|
-
if (fs.existsSync(BACKUP)) {
|
|
739
|
-
console.log('[patch-mentions] Restoring ' + FILE + ' from backup...');
|
|
740
|
-
fs.copyFileSync(BACKUP, FILE);
|
|
741
|
-
restoredCount++;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
console.log('[patch-mentions] Restored ' + restoredCount + ' file(s) successfully.');
|
|
745
|
-
process.exit(0);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
for (const FILE of filesToPatch) {
|
|
749
|
-
const BACKUP = FILE + '.bak';
|
|
750
|
-
if (fs.existsSync(BACKUP)) {
|
|
751
|
-
console.log('[patch-mentions] Restoring ' + FILE + ' from backup to ensure clean patch...');
|
|
752
|
-
fs.copyFileSync(BACKUP, FILE);
|
|
753
|
-
} else {
|
|
754
|
-
console.log('[patch-mentions] Creating backup for ' + FILE);
|
|
755
|
-
fs.copyFileSync(FILE, BACKUP);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
let code = fs.readFileSync(FILE, 'utf8');
|
|
759
|
-
|
|
760
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
761
|
-
// PATCH 1: Helper Functions, Cache & Sticker Delay
|
|
762
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
763
|
-
const HELPER = \`
|
|
764
|
-
// ── [PATCH] Auto-Mention & Sticker Resolution for outgoing group messages ──
|
|
765
|
-
const _mentionMemberCache = new Map();
|
|
766
|
-
const _lastGroupSender = new Map();
|
|
767
|
-
const _MENTION_CACHE_TTL = 5 * 60 * 1000;
|
|
768
|
-
|
|
769
|
-
function _saveActiveSenderToCache(threadId, name, uid) {
|
|
770
|
-
if (!threadId || !name || !uid) return;
|
|
771
|
-
let cached = _mentionMemberCache.get(threadId);
|
|
772
|
-
if (!cached) {
|
|
773
|
-
cached = { ts: Date.now(), membersMap: {} };
|
|
774
|
-
_mentionMemberCache.set(threadId, cached);
|
|
775
|
-
}
|
|
776
|
-
cached.membersMap[name.toString().trim().toLowerCase()] = uid.toString();
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
async function _sendStickerAfterDelay(api, threadId, type, stickerConfig) {
|
|
780
|
-
if (!stickerConfig) return;
|
|
781
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
782
|
-
try {
|
|
783
|
-
let finalSticker = null;
|
|
784
|
-
if (stickerConfig.keyword) {
|
|
785
|
-
let kw = stickerConfig.keyword.trim().toLowerCase();
|
|
786
|
-
const kwMap = {
|
|
787
|
-
"cười": "haha",
|
|
788
|
-
"cuoi": "haha",
|
|
789
|
-
"há há": "haha",
|
|
790
|
-
"ha ha": "haha",
|
|
791
|
-
"kaka": "haha",
|
|
792
|
-
"ôm tim": "love",
|
|
793
|
-
"om tim": "love",
|
|
794
|
-
"tim": "love",
|
|
795
|
-
"yêu": "love",
|
|
796
|
-
"yeu": "love",
|
|
797
|
-
"cà khịa": "ca khia",
|
|
798
|
-
"lêu lêu": "leu leu",
|
|
799
|
-
"tức giận": "tuc gian",
|
|
800
|
-
"tức": "tuc gian",
|
|
801
|
-
"giận": "tuc gian",
|
|
802
|
-
"angry": "tuc gian",
|
|
803
|
-
"buồn": "sad",
|
|
804
|
-
"chào": "hi",
|
|
805
|
-
"hello": "hi",
|
|
806
|
-
"cúi đầu": "thank you",
|
|
807
|
-
"cảm ơn": "thank you",
|
|
808
|
-
"cảm on": "thank you",
|
|
809
|
-
"cam on": "thank you"
|
|
810
|
-
};
|
|
811
|
-
if (kwMap[kw]) {
|
|
812
|
-
console.log('[patch-mentions] Mapping keyword "' + kw + '" to "' + kwMap[kw] + '"');
|
|
813
|
-
kw = kwMap[kw];
|
|
814
|
-
}
|
|
815
|
-
console.log('[patch-mentions] Searching sticker for keyword:', kw);
|
|
816
|
-
const stickerIds = await api.getStickers(kw);
|
|
817
|
-
if (stickerIds && stickerIds.length > 0) {
|
|
818
|
-
const details = await api.getStickersDetail(stickerIds.slice(0, 5));
|
|
819
|
-
if (details && details.length > 0) {
|
|
820
|
-
const selected = details[0];
|
|
821
|
-
finalSticker = {
|
|
822
|
-
id: selected.id,
|
|
823
|
-
cateId: selected.cateId,
|
|
824
|
-
type: selected.type
|
|
825
|
-
};
|
|
826
|
-
console.log('[patch-mentions] Resolved keyword to sticker:', JSON.stringify(finalSticker));
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
if (!finalSticker) {
|
|
830
|
-
console.warn('[patch-mentions] No sticker found for keyword:', kw);
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
} else {
|
|
834
|
-
finalSticker = {
|
|
835
|
-
id: stickerConfig.id,
|
|
836
|
-
cateId: stickerConfig.cateId,
|
|
837
|
-
type: stickerConfig.type
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
await api.sendSticker(finalSticker, threadId, type);
|
|
842
|
-
console.log('[patch-mentions] Sticker sent successfully:', finalSticker.id, 'with type:', finalSticker.type);
|
|
843
|
-
} catch (err) {
|
|
844
|
-
console.error("[patch-mentions] Failed to send sticker:", err);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
async function _resolveAutoMentions(api, threadId, text, isGroup) {
|
|
849
|
-
if (!isGroup || !text) return null;
|
|
850
|
-
|
|
851
|
-
let membersMap = null;
|
|
852
|
-
let cached = _mentionMemberCache.get(threadId);
|
|
853
|
-
if (cached && (Date.now() - cached.ts < _MENTION_CACHE_TTL)) {
|
|
854
|
-
membersMap = cached.membersMap;
|
|
855
|
-
} else {
|
|
856
|
-
try {
|
|
857
|
-
const info = (await api.getGroupInfo(threadId)).gridInfoMap;
|
|
858
|
-
const gInfo = info ? info[threadId] : null;
|
|
859
|
-
if (gInfo && Array.isArray(gInfo.currentMems)) {
|
|
860
|
-
membersMap = {};
|
|
861
|
-
for (const member of gInfo.currentMems) {
|
|
862
|
-
if (!member) continue;
|
|
863
|
-
const id = member.id || member.uid;
|
|
864
|
-
const name = member.dName || member.zaloName;
|
|
865
|
-
if (id && name) {
|
|
866
|
-
membersMap[name.toString().trim().toLowerCase()] = id.toString();
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
_mentionMemberCache.set(threadId, {
|
|
870
|
-
ts: Date.now(),
|
|
871
|
-
membersMap
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
} catch (e) {
|
|
875
|
-
console.error("[patch-mentions] Failed to fetch group members for mention resolution:", e);
|
|
876
|
-
if (cached) membersMap = cached.membersMap;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
if (!membersMap) return null;
|
|
881
|
-
|
|
882
|
-
// Add "all" and "cả nhóm" to membersMap pointing to "-1"
|
|
883
|
-
membersMap["all"] = "-1";
|
|
884
|
-
membersMap["cả nhóm"] = "-1";
|
|
885
|
-
|
|
886
|
-
// Sort names by length descending
|
|
887
|
-
const sortedNames = Object.keys(membersMap).sort((a, b) => b.length - a.length);
|
|
888
|
-
|
|
889
|
-
const mentions = [];
|
|
890
|
-
let maskedText = text;
|
|
891
|
-
|
|
892
|
-
for (const name of sortedNames) {
|
|
893
|
-
const escapedName = name.replace(/[-\\\\/\\\\\\\\^\$*+?.()|[\\\\]{}]/g, '\\\\\\\\\$&');
|
|
894
|
-
const regex = new RegExp('@' + escapedName + '(?!\\\\\\\\p{L}|\\\\\\\\p{N})', 'gui');
|
|
895
|
-
|
|
896
|
-
let match;
|
|
897
|
-
while ((match = regex.exec(maskedText)) !== null) {
|
|
898
|
-
const pos = match.index;
|
|
899
|
-
const matchedString = match[0];
|
|
900
|
-
const len = matchedString.length;
|
|
901
|
-
const uid = membersMap[name];
|
|
902
|
-
|
|
903
|
-
mentions.push({ pos, len, uid });
|
|
904
|
-
maskedText = maskedText.substring(0, pos) + ' '.repeat(len) + maskedText.substring(pos + len);
|
|
905
|
-
regex.lastIndex = 0;
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
mentions.sort((a, b) => a.pos - b.pos);
|
|
910
|
-
return mentions.length > 0 ? mentions : null;
|
|
911
|
-
}
|
|
912
|
-
\`;
|
|
913
|
-
|
|
914
|
-
// Insert HELPER before toInboundMessage
|
|
915
|
-
const TO_INBOUND_ANCHOR = 'function toInboundMessage(message, ownUserId) {';
|
|
916
|
-
if (!code.includes(TO_INBOUND_ANCHOR)) {
|
|
917
|
-
console.error('[patch-mentions] Error: TO_INBOUND_ANCHOR not found!');
|
|
918
|
-
process.exit(1);
|
|
919
|
-
}
|
|
920
|
-
code = code.replace(TO_INBOUND_ANCHOR, () => HELPER + '\\n' + TO_INBOUND_ANCHOR);
|
|
921
|
-
|
|
922
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
923
|
-
// PATCH 2: Save active sender and track last sender of group
|
|
924
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
925
|
-
const INBOUND_RESOLVE_ANCHOR = \`\\tconst senderId = toNumberId(data.uidFrom);
|
|
926
|
-
\\tconst threadId = isGroup ? toNumberId(data.idTo) : toNumberId(data.uidFrom) || toNumberId(data.idTo);\`;
|
|
927
|
-
|
|
928
|
-
const INBOUND_RESOLVE_PATCH = \`\\tconst senderId = toNumberId(data.uidFrom);
|
|
929
|
-
\\tconst threadId = isGroup ? toNumberId(data.idTo) : toNumberId(data.uidFrom) || toNumberId(data.idTo);
|
|
930
|
-
\\t// [PATCH] Save sender info and track last active sender (excluding bot itself)
|
|
931
|
-
\\tif (isGroup && senderId && data.dName && threadId && senderId.toString() !== ownUserId?.toString()) {
|
|
932
|
-
\\t\\t_saveActiveSenderToCache(threadId, data.dName, senderId);
|
|
933
|
-
\\t\\t_lastGroupSender.set(threadId, {
|
|
934
|
-
\\t\\t\\tuid: senderId.toString(),
|
|
935
|
-
\\t\\t\\tname: data.dName.toString().trim(),
|
|
936
|
-
\\t\\t\\ttimestamp: Date.now()
|
|
937
|
-
\\t\\t});
|
|
938
|
-
\\t}\`;
|
|
939
|
-
|
|
940
|
-
if (!code.includes(INBOUND_RESOLVE_ANCHOR)) {
|
|
941
|
-
console.error('[patch-mentions] Error: INBOUND_RESOLVE_ANCHOR not found!');
|
|
942
|
-
process.exit(1);
|
|
943
|
-
}
|
|
944
|
-
code = code.replace(INBOUND_RESOLVE_ANCHOR, () => INBOUND_RESOLVE_PATCH);
|
|
945
|
-
|
|
946
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
947
|
-
// PATCH 3: Add sticker config parser to sendZaloTextMessage start
|
|
948
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
949
|
-
const SEND_TEXT_FUNC_ANCHOR = 'async function sendZaloTextMessage(threadId, text, options = {}) {';
|
|
950
|
-
const SEND_TEXT_FUNC_PATCH = \`async function sendZaloTextMessage(threadId, text, options = {}) {
|
|
951
|
-
// [PATCH] Parse sticker config and strip from text
|
|
952
|
-
let stickerConfig = null;
|
|
953
|
-
const stickerRegex = /\\\\[Sticker:\\\\s*([^\\\\]]+)\\\\]/i;
|
|
954
|
-
if (text && typeof text === 'string') {
|
|
955
|
-
const match = text.match(stickerRegex);
|
|
956
|
-
if (match) {
|
|
957
|
-
const parts = match[1].split(':');
|
|
958
|
-
if (parts.length >= 2) {
|
|
959
|
-
stickerConfig = {
|
|
960
|
-
id: parts[0].trim(),
|
|
961
|
-
cateId: parts[1].trim(),
|
|
962
|
-
type: parts[2] ? parseInt(parts[2].trim(), 10) : 30
|
|
963
|
-
};
|
|
964
|
-
} else {
|
|
965
|
-
stickerConfig = {
|
|
966
|
-
keyword: parts[0].trim()
|
|
967
|
-
};
|
|
968
|
-
}
|
|
969
|
-
text = text.replace(stickerRegex, '').trim();
|
|
970
|
-
}
|
|
971
|
-
}\`;
|
|
972
|
-
|
|
973
|
-
if (!code.includes(SEND_TEXT_FUNC_ANCHOR)) {
|
|
974
|
-
console.error('[patch-mentions] Error: SEND_TEXT_FUNC_ANCHOR not found!');
|
|
975
|
-
process.exit(1);
|
|
976
|
-
}
|
|
977
|
-
code = code.replace(SEND_TEXT_FUNC_ANCHOR, () => SEND_TEXT_FUNC_PATCH);
|
|
978
|
-
|
|
979
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
980
|
-
// PATCH 4: Intercept sendZaloTextMessage and resolve auto-tagging
|
|
981
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
982
|
-
|
|
983
|
-
// 4a. In mediaUrl path (const media to const textStyles):
|
|
984
|
-
const MEDIA_URL_RESOLVE_ANCHOR = \`\\t\\t\\t\\tconst media = await loadOutboundMediaFromUrl(options.mediaUrl.trim(), {
|
|
985
|
-
\\t\\t\\t\\t\\tmediaLocalRoots: options.mediaLocalRoots,
|
|
986
|
-
\\t\\t\\t\\t\\tmediaReadFile: options.mediaReadFile
|
|
987
|
-
\\t\\t\\t\\t});
|
|
988
|
-
\\t\\t\\t\\tconst fileName = resolveMediaFileName({
|
|
989
|
-
\\t\\t\\t\\t\\tmediaUrl: options.mediaUrl,
|
|
990
|
-
\\t\\t\\t\\t\\tfileName: media.fileName,
|
|
991
|
-
\\t\\t\\t\\t\\tcontentType: media.contentType,
|
|
992
|
-
\\t\\t\\t\\t\\tkind: media.kind
|
|
993
|
-
\\t\\t\\t\\t});
|
|
994
|
-
\\t\\t\\t\\tconst payloadText = (text || options.caption || "").slice(0, 2e3);
|
|
995
|
-
\\t\\t\\t\\tconst textStyles = clampTextStyles(payloadText, options.textStyles);\`;
|
|
996
|
-
|
|
997
|
-
const MEDIA_URL_RESOLVE_PATCH = \`\\t\\t\\t\\tconst media = await loadOutboundMediaFromUrl(options.mediaUrl.trim(), {
|
|
998
|
-
\\t\\t\\t\\t\\tmediaLocalRoots: options.mediaLocalRoots,
|
|
999
|
-
\\t\\t\\t\\t\\tmediaReadFile: options.mediaReadFile
|
|
1000
|
-
\\t\\t\\t\\t});
|
|
1001
|
-
\\t\\t\\t\\tconst fileName = resolveMediaFileName({
|
|
1002
|
-
\\t\\t\\t\\t\\tmediaUrl: options.mediaUrl,
|
|
1003
|
-
\\t\\t\\t\\t\\tfileName: media.fileName,
|
|
1004
|
-
\\t\\t\\t\\t\\tcontentType: media.contentType,
|
|
1005
|
-
\\t\\t\\t\\t\\tkind: media.kind
|
|
1006
|
-
\\t\\t\\t\\t});
|
|
1007
|
-
\\t\\t\\t\\tlet payloadText = (text || options.caption || "").slice(0, 2e3);
|
|
1008
|
-
\\t\\t\\t\\tconst textStyles = clampTextStyles(payloadText, options.textStyles);
|
|
1009
|
-
\\t\\t\\t\\tlet mentions = null;
|
|
1010
|
-
\\t\\t\\t\\tif (options.isGroup) {
|
|
1011
|
-
\\t\\t\\t\\t\\tmentions = await _resolveAutoMentions(api, trimmedThreadId, payloadText, options.isGroup);
|
|
1012
|
-
\\t\\t\\t\\t\\tconst lastSender = _lastGroupSender.get(trimmedThreadId);
|
|
1013
|
-
\\t\\t\\t\\t\\tif (lastSender && (Date.now() - lastSender.timestamp < 5 * 60 * 1000)) {
|
|
1014
|
-
\\t\\t\\t\\t\\t\\tconst alreadyTagged = mentions && mentions.some(m => m.uid === lastSender.uid);
|
|
1015
|
-
\\t\\t\\t\\t\\t\\tif (!alreadyTagged) {
|
|
1016
|
-
\\t\\t\\t\\t\\t\\t\\tconst tagText = \\\`@\\\${lastSender.name} \\\`;
|
|
1017
|
-
\\t\\t\\t\\t\\t\\t\\tpayloadText = tagText + payloadText;
|
|
1018
|
-
\\t\\t\\t\\t\\t\\t\\tconst newMention = {
|
|
1019
|
-
\\t\\t\\t\\t\\t\\t\\t\\tpos: 0,
|
|
1020
|
-
\\t\\t\\t\\t\\t\\t\\t\\tlen: tagText.length - 1,
|
|
1021
|
-
\\t\\t\\t\\t\\t\\t\\t\\tuid: lastSender.uid
|
|
1022
|
-
\\t\\t\\t\\t\\t\\t\\t};
|
|
1023
|
-
\\t\\t\\t\\t\\t\\t\\tif (mentions) {
|
|
1024
|
-
\\t\\t\\t\\t\\t\\t\\t\\tmentions = mentions.map(m => ({ ...m, pos: m.pos + tagText.length }));
|
|
1025
|
-
\\t\\t\\t\\t\\t\\t\\t\\tmentions.unshift(newMention);
|
|
1026
|
-
\\t\\t\\t\\t\\t\\t\\t} else {
|
|
1027
|
-
\\t\\t\\t\\t\\t\\t\\t\\tmentions = [newMention];
|
|
1028
|
-
\\t\\t\\t\\t\\t\\t\\t}
|
|
1029
|
-
\\t\\t\\t\\t\\t\\t\\tif (textStyles) {
|
|
1030
|
-
\\t\\t\\t\\t\\t\\t\\t\\tfor (const style of textStyles) {
|
|
1031
|
-
\\t\\t\\t\\t\\t\\t\\t\\t\\tif (style && typeof style.start === 'number') {
|
|
1032
|
-
\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tstyle.start += tagText.length;
|
|
1033
|
-
\\t\\t\\t\\t\\t\\t\\t\\t\\t}
|
|
1034
|
-
\\t\\t\\t\\t\\t\\t\\t\\t}
|
|
1035
|
-
\\t\\t\\t\\t\\t\\t\\t}
|
|
1036
|
-
\\t\\t\\t\\t\\t\\t}
|
|
1037
|
-
\\t\\t\\t\\t\\t}
|
|
1038
|
-
\\t\\t\\t\\t}\`;
|
|
1039
|
-
|
|
1040
|
-
if (!code.includes(MEDIA_URL_RESOLVE_ANCHOR)) {
|
|
1041
|
-
console.error('[patch-mentions] Error: MEDIA_URL_RESOLVE_ANCHOR not found!');
|
|
1042
|
-
process.exit(1);
|
|
1043
|
-
}
|
|
1044
|
-
code = code.replace(MEDIA_URL_RESOLVE_ANCHOR, () => MEDIA_URL_RESOLVE_PATCH);
|
|
1045
|
-
|
|
1046
|
-
// 4b. Inject mentions to api.sendMessage with attachments & send sticker:
|
|
1047
|
-
const MEDIA_SEND_MESSAGE_ANCHOR = \`\\t\\t\\t\\tconst messageId = extractSendMessageId(await api.sendMessage({
|
|
1048
|
-
\\t\\t\\t\\t\\tmsg: payloadText,
|
|
1049
|
-
\\t\\t\\t\\t\\t...textStyles ? { styles: textStyles } : {},
|
|
1050
|
-
\\t\\t\\t\\t\\tattachments: [{
|
|
1051
|
-
\\t\\t\\t\\t\\t\\tdata: media.buffer,
|
|
1052
|
-
\\t\\t\\t\\t\\t\\tfilename: fileName.includes(".") ? fileName : \\\`\\\${fileName}.bin\\\`,
|
|
1053
|
-
\\t\\t\\t\\t\\t\\tmetadata: { totalSize: media.buffer.length }
|
|
1054
|
-
\\t\\t\\t\\t\\t}]
|
|
1055
|
-
\\t\\t\\t\\t}, trimmedThreadId, type));
|
|
1056
|
-
\\t\\t\\t\\treturn {
|
|
1057
|
-
\\t\\t\\t\\t\\tok: true,
|
|
1058
|
-
\\t\\t\\t\\t\\tmessageId,
|
|
1059
|
-
\\t\\t\\t\\t\\treceipt: createZalouserSendReceipt({
|
|
1060
|
-
\\t\\t\\t\\t\\t\\tmessageId,
|
|
1061
|
-
\\t\\t\\t\\t\\t\\tthreadId: trimmedThreadId,
|
|
1062
|
-
\\t\\t\\t\\t\\t\\tkind: "media"
|
|
1063
|
-
\\t\\t\\t\\t\\t})
|
|
1064
|
-
\\t\\t\\t\\t};\`;
|
|
1065
|
-
|
|
1066
|
-
const MEDIA_SEND_MESSAGE_PATCH = \`\\t\\t\\t\\tconst messageId = extractSendMessageId(await api.sendMessage({
|
|
1067
|
-
\\t\\t\\t\\t\\tmsg: payloadText,
|
|
1068
|
-
\\t\\t\\t\\t\\t...textStyles ? { styles: textStyles } : {},
|
|
1069
|
-
\\t\\t\\t\\t\\t...(mentions ? { mentions } : {}),
|
|
1070
|
-
\\t\\t\\t\\t\\tattachments: [{
|
|
1071
|
-
\\t\\t\\t\\t\\t\\tdata: media.buffer,
|
|
1072
|
-
\\t\\t\\t\\t\\t\\tfilename: fileName.includes(".") ? fileName : \\\`\\\${fileName}.bin\\\`,
|
|
1073
|
-
\\t\\t\\t\\t\\t\\tmetadata: { totalSize: media.buffer.length }
|
|
1074
|
-
\\t\\t\\t\\t\\t}]
|
|
1075
|
-
\\t\\t\\t\\t}, trimmedThreadId, type));
|
|
1076
|
-
\\t\\t\\t\\tif (stickerConfig) {
|
|
1077
|
-
\\t\\t\\t\\t\\tawait _sendStickerAfterDelay(api, trimmedThreadId, type, stickerConfig);
|
|
1078
|
-
\\t\\t\\t\\t}
|
|
1079
|
-
\\t\\t\\t\\treturn {
|
|
1080
|
-
\\t\\t\\t\\t\\tok: true,
|
|
1081
|
-
\\t\\t\\t\\t\\tmessageId,
|
|
1082
|
-
\\t\\t\\t\\t\\treceipt: createZalouserSendReceipt({
|
|
1083
|
-
\\t\\t\\t\\t\\t\\tmessageId,
|
|
1084
|
-
\\t\\t\\t\\t\\t\\tthreadId: trimmedThreadId,
|
|
1085
|
-
\\t\\t\\t\\t\\t\\tkind: "media"
|
|
1086
|
-
\\t\\t\\t\\t\\t})
|
|
1087
|
-
\\t\\t\\t\\t};\`;
|
|
1088
|
-
|
|
1089
|
-
if (!code.includes(MEDIA_SEND_MESSAGE_ANCHOR)) {
|
|
1090
|
-
console.error('[patch-mentions] Error: MEDIA_SEND_MESSAGE_ANCHOR not found!');
|
|
1091
|
-
process.exit(1);
|
|
1092
|
-
}
|
|
1093
|
-
code = code.replace(MEDIA_SEND_MESSAGE_ANCHOR, () => MEDIA_SEND_MESSAGE_PATCH);
|
|
1094
|
-
|
|
1095
|
-
// 4c. In plain-text path (auto-tag & send sticker):
|
|
1096
|
-
const PLAIN_TEXT_SEND_ANCHOR = \`\\t\\t\\tconst payloadText = text.slice(0, 2e3);
|
|
1097
|
-
\\t\\t\\tconst textStyles = clampTextStyles(payloadText, options.textStyles);
|
|
1098
|
-
\\t\\t\\tconst messageId = extractSendMessageId(await api.sendMessage(textStyles ? {
|
|
1099
|
-
\\t\\t\\t\\tmsg: payloadText,
|
|
1100
|
-
\\t\\t\\t\\tstyles: textStyles
|
|
1101
|
-
\\t\\t\\t} : payloadText, trimmedThreadId, type));
|
|
1102
|
-
\\t\\t\\treturn {
|
|
1103
|
-
\\t\\t\\t\\tok: true,
|
|
1104
|
-
\\t\\t\\t\\tmessageId,
|
|
1105
|
-
\\t\\t\\t\\treceipt: createZalouserSendReceipt({
|
|
1106
|
-
\\t\\t\\t\\t\\tmessageId,
|
|
1107
|
-
\\t\\t\\t\\t\\tthreadId: trimmedThreadId,
|
|
1108
|
-
\\t\\t\\t\\t\\tkind: "text"
|
|
1109
|
-
\\t\\t\\t\\t})
|
|
1110
|
-
\\t\\t\\t};\`;
|
|
1111
|
-
|
|
1112
|
-
const PLAIN_TEXT_SEND_PATCH = \`\\t\\t\\tlet payloadText = text.slice(0, 2e3);
|
|
1113
|
-
\\t\\t\\tconst textStyles = clampTextStyles(payloadText, options.textStyles);
|
|
1114
|
-
\\t\\t\\tlet mentions = null;
|
|
1115
|
-
\\t\\t\\tif (options.isGroup) {
|
|
1116
|
-
\\t\\t\\t\\tmentions = await _resolveAutoMentions(api, trimmedThreadId, payloadText, options.isGroup);
|
|
1117
|
-
\\t\\t\\t\\tconst lastSender = _lastGroupSender.get(trimmedThreadId);
|
|
1118
|
-
\\t\\t\\t\\tif (lastSender && (Date.now() - lastSender.timestamp < 5 * 60 * 1000)) {
|
|
1119
|
-
\\t\\t\\t\\t\\tconst alreadyTagged = mentions && mentions.some(m => m.uid === lastSender.uid);
|
|
1120
|
-
\\t\\t\\t\\t\\tif (!alreadyTagged) {
|
|
1121
|
-
\\t\\t\\t\\t\\t\\tconst tagText = \\\`@\\\${lastSender.name} \\\`;
|
|
1122
|
-
\\t\\t\\t\\t\\t\\tpayloadText = tagText + payloadText;
|
|
1123
|
-
\\t\\t\\t\\t\\t\\tconst newMention = {
|
|
1124
|
-
\\t\\t\\t\\t\\t\\t\\tpos: 0,
|
|
1125
|
-
\\t\\t\\t\\t\\t\\t\\tlen: tagText.length - 1,
|
|
1126
|
-
\\t\\t\\t\\t\\t\\t\\tuid: lastSender.uid
|
|
1127
|
-
\\t\\t\\t\\t\\t\\t};
|
|
1128
|
-
\\t\\t\\t\\t\\t\\tif (mentions) {
|
|
1129
|
-
\\t\\t\\t\\t\\t\\t\\tmentions = mentions.map(m => ({ ...m, pos: m.pos + tagText.length }));
|
|
1130
|
-
\\t\\t\\t\\t\\t\\t\\tmentions.unshift(newMention);
|
|
1131
|
-
\\t\\t\\t\\t\\t\\t} else {
|
|
1132
|
-
\\t\\t\\t\\t\\t\\t\\tmentions = [newMention];
|
|
1133
|
-
\\t\\t\\t\\t\\t\\t}
|
|
1134
|
-
\\t\\t\\t\\t\\t\\tif (textStyles) {
|
|
1135
|
-
\\t\\t\\t\\t\\t\\t\\tfor (const style of textStyles) {
|
|
1136
|
-
\\t\\t\\t\\t\\t\\t\\t\\tif (style && typeof style.start === 'number') {
|
|
1137
|
-
\\t\\t\\t\\t\\t\\t\\t\\t\\tstyle.start += tagText.length;
|
|
1138
|
-
\\t\\t\\t\\t\\t\\t\\t\\t}
|
|
1139
|
-
\\t\\t\\t\\t\\t\\t\\t}
|
|
1140
|
-
\\t\\t\\t\\t\\t\\t}
|
|
1141
|
-
\\t\\t\\t\\t\\t}
|
|
1142
|
-
\\t\\t\\t\\t}
|
|
1143
|
-
\\t\\t\\t}
|
|
1144
|
-
\\t\\t\\tconst messageObj = {
|
|
1145
|
-
\\t\\t\\t\\tmsg: payloadText,
|
|
1146
|
-
\\t\\t\\t\\t...(textStyles ? { styles: textStyles } : {}),
|
|
1147
|
-
\\t\\t\\t\\t...(mentions ? { mentions } : {})
|
|
1148
|
-
\\t\\t\\t};
|
|
1149
|
-
\\t\\t\\tconst messageId = extractSendMessageId(await api.sendMessage(messageObj, trimmedThreadId, type));
|
|
1150
|
-
\\t\\t\\tif (stickerConfig) {
|
|
1151
|
-
\\t\\t\\t\\tawait _sendStickerAfterDelay(api, trimmedThreadId, type, stickerConfig);
|
|
1152
|
-
\\t\\t\\t}
|
|
1153
|
-
\\t\\t\\treturn {
|
|
1154
|
-
\\t\\t\\t\\tok: true,
|
|
1155
|
-
\\t\\t\\t\\tmessageId,
|
|
1156
|
-
\\t\\t\\t\\treceipt: createZalouserSendReceipt({
|
|
1157
|
-
\\t\\t\\t\\t\\tmessageId,
|
|
1158
|
-
\\t\\t\\t\\t\\tthreadId: trimmedThreadId,
|
|
1159
|
-
\\t\\t\\t\\t\\tkind: "text"
|
|
1160
|
-
\\t\\t\\t\\t})
|
|
1161
|
-
\\t\\t\\t};\`;
|
|
1162
|
-
|
|
1163
|
-
if (!code.includes(PLAIN_TEXT_SEND_ANCHOR)) {
|
|
1164
|
-
console.error('[patch-mentions] Error: PLAIN_TEXT_SEND_ANCHOR not found!');
|
|
1165
|
-
process.exit(1);
|
|
1166
|
-
}
|
|
1167
|
-
code = code.replace(PLAIN_TEXT_SEND_ANCHOR, () => PLAIN_TEXT_SEND_PATCH);
|
|
1168
|
-
|
|
1169
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1170
|
-
// PATCH 5: Intercept sendZaloTextMessage with sticker search command
|
|
1171
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1172
|
-
const WITH_ZALO_API_ANCHOR = \`\\treturn await withZaloApi(profile, async (api) => {
|
|
1173
|
-
\\t\\tconst type = options.isGroup ? ThreadType.Group : ThreadType.User;\`;
|
|
1174
|
-
|
|
1175
|
-
const WITH_ZALO_API_PATCH = \`\\treturn await withZaloApi(profile, async (api) => {
|
|
1176
|
-
\\t\\t// [PATCH] Temporary sticker search command interceptor
|
|
1177
|
-
\\t\\tif (text && text.startsWith('/search-sticker ')) {
|
|
1178
|
-
\\t\\t\\tconst keyword = text.replace('/search-sticker ', '').trim();
|
|
1179
|
-
\\t\\t\\ttry {
|
|
1180
|
-
\\t\\t\\t\\tconst stickerIds = await api.getStickers(keyword);
|
|
1181
|
-
\\t\\t\\t\\tif (!stickerIds || stickerIds.length === 0) {
|
|
1182
|
-
\\t\\t\\t\\t\\ttext = "Không tìm thấy sticker nào cho từ khóa này.";
|
|
1183
|
-
\\t\\t\\t\\t} else {
|
|
1184
|
-
\\t\\t\\t\\t\\tconst details = await api.getStickersDetail(stickerIds.slice(0, 20));
|
|
1185
|
-
\\t\\t\\t\\t\\tconst list = details.map(d => \\\`- \\\${d.text || 'sticker'}: [Sticker: \\\${d.id}:\\\${d.cateId}] (id: \\\${d.id}, cateId: \\\${d.cateId})\\\`).join('\\\\n');
|
|
1186
|
-
\\t\\t\\t\\t\\ttext = "Kết quả tìm kiếm sticker cho '" + keyword + "':\\\\n" + list;
|
|
1187
|
-
\\t\\t\\t\\t}
|
|
1188
|
-
\\t\\t\\t} catch (e) {
|
|
1189
|
-
\\t\\t\\t\\ttext = "Lỗi khi tìm sticker: " + e.message;
|
|
1190
|
-
\\t\\t\\t}
|
|
1191
|
-
\\t\\t}
|
|
1192
|
-
\\t\\tconst type = options.isGroup ? ThreadType.Group : ThreadType.User;\`;
|
|
1193
|
-
|
|
1194
|
-
if (!code.includes(WITH_ZALO_API_ANCHOR)) {
|
|
1195
|
-
console.error('[patch-mentions] Error: WITH_ZALO_API_ANCHOR not found!');
|
|
1196
|
-
process.exit(1);
|
|
1197
|
-
}
|
|
1198
|
-
code = code.replace(WITH_ZALO_API_ANCHOR, () => WITH_ZALO_API_PATCH);
|
|
1199
|
-
|
|
1200
|
-
// Write modified file
|
|
1201
|
-
fs.writeFileSync(FILE, code, 'utf8');
|
|
1202
|
-
console.log('[patch-mentions] File successfully patched: ' + FILE);
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
`;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
390
|
+
function buildInfographicGeneratorSkillMd(botName = 'Williams') {
|
|
391
|
+
return '';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function buildInfographicGeneratorJs() {
|
|
395
|
+
return '';
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function buildStickerMentionSkillMd(botName = 'Williams') {
|
|
399
|
+
return '';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function buildStickerMentionJs() {
|
|
403
|
+
return '';
|
|
404
|
+
}
|
|
405
|
+
|
|
1211
406
|
function buildSecurityRules(isVi = true) {
|
|
1212
407
|
if (isVi) {
|
|
1213
408
|
return `\n\n## 🔐 Quy Tắc Bảo Mật — BẮT BUỘC (Red Lines)\n\n**GIỚI HẠN FILE & HỆ THỐNG:**\n- ✅ CHỈ làm việc trong thư mục project (workspace).\n- ❌ KHÔNG cung cấp file nội bộ của \`.openclaw\` (config.json, registry.json, task-memory.json, workspace-memory...)\n- ❌ KHÔNG đọc, sao chép, hoặc truy cập bất kỳ file nào ngoài thư mục project\n- ❌ KHÔNG quét hoặc liệt kê các thư mục hệ thống: Documents, Desktop, Downloads, AppData\n- ❌ KHÔNG truy cập registry, system32, hoặc Program Files\n- ❌ KHÔNG cài đặt phần mềm, driver, hoặc service ngoài Docker\n\n**API KEY & CREDENTIALS:**\n- ❌ KHÔNG BAO GIỜ hiển thị API key, token, hoặc mật khẩu trong chat\n- ❌ KHÔNG viết API key trực tiếp vào mã nguồn\n- ❌ KHÔNG commit file credentials lên Git\n- ✅ LUÔN lưu credentials trong file .env riêng\n- ✅ LUÔN dùng biến môi trường thay vì hardcode\n\n**VÍ CRYPTO & TÀI SẢN SỐ:**\n- ❌ TUYỆT ĐỐI KHÔNG truy cập, đọc, hoặc quét các thư mục ví crypto\n- ❌ KHÔNG quét clipboard (có thể chứa seed phrases)\n- ❌ KHÔNG truy cập browser profile, cookie, hoặc mật khẩu đã lưu\n- ❌ KHÔNG cài đặt npm package lạ (chỉ openclaw và plugin chính thức)\n\n**DOCKER:**\n- ✅ Chỉ mount đúng thư mục cần thiết (config + workspace)\n- ❌ KHÔNG mount nguyên ổ đĩa (C:/ hoặc D:/)\n- ❌ KHÔNG chạy container với --privileged\n- ✅ Giới hạn port expose (chỉ 18789)`;
|
package/dist/web/app.js
CHANGED
|
@@ -268,7 +268,18 @@ async function pickFolderPathShared() {
|
|
|
268
268
|
const picked = await api('/api/project/pick-folder', { method: 'POST', body: {} });
|
|
269
269
|
const projectDir = String(picked.projectDir || '').trim();
|
|
270
270
|
if (projectDir) return { projectDir };
|
|
271
|
-
} catch {
|
|
271
|
+
} catch (err) {
|
|
272
|
+
return new Promise((resolve) => {
|
|
273
|
+
openPathModal({
|
|
274
|
+
title: t('Nhập đường dẫn project', 'Enter project path'),
|
|
275
|
+
message: t('Vui lòng nhập đường dẫn tuyệt đối đến thư mục project (ví dụ: /Users/mac/my-bot)', 'Please enter the absolute path to your project directory (e.g. /Users/mac/my-bot)'),
|
|
276
|
+
value: state.install?.projectDir || '',
|
|
277
|
+
placeholder: state.os === 'macos' ? '/Users/your-name/my-bot' : '/home/your-name/my-bot',
|
|
278
|
+
onConfirm: (val) => resolve({ projectDir: val }),
|
|
279
|
+
onCancel: () => resolve(null)
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
272
283
|
return null;
|
|
273
284
|
}
|
|
274
285
|
|
|
@@ -941,6 +952,7 @@ function wireTab() {
|
|
|
941
952
|
state.activeBotId = '';
|
|
942
953
|
state.selectedFile = '';
|
|
943
954
|
state.files = [];
|
|
955
|
+
await loadSystem(true);
|
|
944
956
|
await loadStatus(true);
|
|
945
957
|
// Auto-switch to the first channel that has bots in the new project
|
|
946
958
|
autoSwitchBotChannel();
|
|
@@ -970,12 +982,18 @@ function wireTab() {
|
|
|
970
982
|
}));
|
|
971
983
|
document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
|
|
972
984
|
const result = await pickFolderPathShared();
|
|
973
|
-
if (!result) throw new Error(t('
|
|
974
|
-
|
|
985
|
+
if (!result) throw new Error(t('Chưa chọn thư mục','No folder selected'));
|
|
986
|
+
await api('/api/project/connect', { method: 'POST', body: { projectDir: result.projectDir } });
|
|
987
|
+
state.projectConnectMessage = `OK ${t('Đã kết nối','Connected')}: ${result.projectDir}`;
|
|
975
988
|
showToast(t('Đã kết nối', 'Connected'), t('Kết nối thành công project: ', 'Successfully connected project: ') + fileBaseName(result.projectDir), 'success');
|
|
976
989
|
state.selectedProjectDir = result.projectDir;
|
|
977
990
|
state.pendingProjectDir = '';
|
|
991
|
+
state.activeBotId = '';
|
|
992
|
+
state.selectedFile = '';
|
|
993
|
+
state.files = [];
|
|
994
|
+
await loadSystem(true);
|
|
978
995
|
await loadStatus(true);
|
|
996
|
+
autoSwitchBotChannel();
|
|
979
997
|
await loadFiles(true);
|
|
980
998
|
await loadFeatureFlags(true);
|
|
981
999
|
state.tab = 'bot';
|
|
@@ -1152,6 +1170,8 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1152
1170
|
if (action === 'ok') {
|
|
1153
1171
|
const value = document.getElementById('path-modal-input')?.value?.trim() || '';
|
|
1154
1172
|
if (value && typeof modal.onConfirm === 'function') modal.onConfirm(value);
|
|
1173
|
+
} else {
|
|
1174
|
+
if (typeof modal.onCancel === 'function') modal.onCancel();
|
|
1155
1175
|
}
|
|
1156
1176
|
state.pathModal = null;
|
|
1157
1177
|
render();
|
|
@@ -1212,7 +1232,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1212
1232
|
}
|
|
1213
1233
|
|
|
1214
1234
|
function escapeHtml(s='') { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
|
1215
|
-
function fileBaseName(s='') {
|
|
1235
|
+
function fileBaseName(s='') { const clean = String(s).replace(/[\\/]+$/, ''); return clean.split(/[\\/]/).pop() || s; }
|
|
1216
1236
|
async function loadSystem(silent=false){ state.system = await api('/api/system'); if (!silent) render(); }
|
|
1217
1237
|
async function loadStatus(silent=false){ state.install = await api('/api/bot/status'); if (!state.selectedProjectDir && state.install?.projectDir) state.selectedProjectDir = state.install.projectDir; if (!silent) render(); }
|
|
1218
1238
|
function autoSwitchBotChannel() {
|