mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-16 23:49:47 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 502d2448b2 | |||
| b9e48d9483 | |||
| b0554f8d0c | |||
| 12f3d95639 |
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||||
"version": "1.28.1"
|
"version": "1.28.3"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
English | [中文](./CHANGELOG.zh.md)
|
||||||
|
|
||||||
|
## 1.28.3 - 2026-02-03
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-post-to-wechat`: fix placeholder matching issue where `WECHATIMGPH_1` incorrectly matched `WECHATIMGPH_10`.
|
||||||
|
|
||||||
|
## 1.28.2 - 2026-02-03
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-post-to-x`: reuse existing Chrome instance when available; fix placeholder matching issue where `XIMGPH_1` incorrectly matched `XIMGPH_10`; improve image sorting by placeholder index; use `execCommand` for more reliable placeholder deletion.
|
||||||
|
|
||||||
## 1.28.1 - 2026-02-02
|
## 1.28.1 - 2026-02-02
|
||||||
|
|
||||||
### Refactor
|
### Refactor
|
||||||
|
|||||||
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[English](./CHANGELOG.md) | 中文
|
||||||
|
|
||||||
|
## 1.28.3 - 2026-02-03
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-post-to-wechat`:修复占位符匹配问题(`WECHATIMGPH_1` 错误匹配 `WECHATIMGPH_10`)。
|
||||||
|
|
||||||
|
## 1.28.2 - 2026-02-03
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-post-to-x`:复用已有 Chrome 实例;修复占位符匹配问题(`XIMGPH_1` 错误匹配 `XIMGPH_10`);改进图片按占位符序号排序;使用 `execCommand` 提高占位符删除可靠性。
|
||||||
|
|
||||||
## 1.28.1 - 2026-02-02
|
## 1.28.1 - 2026-02-02
|
||||||
|
|
||||||
### 重构
|
### 重构
|
||||||
|
|||||||
@@ -273,22 +273,31 @@ async function selectAndReplacePlaceholder(session: ChromeSession, placeholder:
|
|||||||
const editor = document.querySelector('.ProseMirror');
|
const editor = document.querySelector('.ProseMirror');
|
||||||
if (!editor) return false;
|
if (!editor) return false;
|
||||||
|
|
||||||
|
const placeholder = ${JSON.stringify(placeholder)};
|
||||||
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT, null, false);
|
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT, null, false);
|
||||||
let node;
|
let node;
|
||||||
|
|
||||||
while ((node = walker.nextNode())) {
|
while ((node = walker.nextNode())) {
|
||||||
const text = node.textContent || '';
|
const text = node.textContent || '';
|
||||||
const idx = text.indexOf(${JSON.stringify(placeholder)});
|
let searchStart = 0;
|
||||||
if (idx !== -1) {
|
let idx;
|
||||||
node.parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
// Search for exact match (not prefix of longer placeholder like XIMGPH_1 in XIMGPH_10)
|
||||||
|
while ((idx = text.indexOf(placeholder, searchStart)) !== -1) {
|
||||||
|
const afterIdx = idx + placeholder.length;
|
||||||
|
const charAfter = text[afterIdx];
|
||||||
|
// Exact match if next char is not a digit
|
||||||
|
if (charAfter === undefined || !/\\d/.test(charAfter)) {
|
||||||
|
node.parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.setStart(node, idx);
|
range.setStart(node, idx);
|
||||||
range.setEnd(node, idx + ${placeholder.length});
|
range.setEnd(node, idx + placeholder.length);
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
sel.addRange(range);
|
sel.addRange(range);
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
searchStart = afterIdx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -61,6 +61,25 @@ interface ArticleOptions {
|
|||||||
chromePath?: string;
|
chromePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findExistingDebugPort(profileDir: string): Promise<number | null> {
|
||||||
|
const portFile = path.join(profileDir, 'DevToolsActivePort');
|
||||||
|
if (!fs.existsSync(portFile)) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(portFile, 'utf-8').trim();
|
||||||
|
if (!content) return null;
|
||||||
|
const [portLine] = content.split(/\r?\n/);
|
||||||
|
const port = Number(portLine);
|
||||||
|
if (!Number.isFinite(port) || port <= 0) return null;
|
||||||
|
|
||||||
|
// Verify the port is actually active.
|
||||||
|
await waitForChromeDebugPort(port, 1500, { includeLastError: true });
|
||||||
|
return port;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function publishArticle(options: ArticleOptions): Promise<void> {
|
export async function publishArticle(options: ArticleOptions): Promise<void> {
|
||||||
const { markdownPath, submit = false, profileDir = getDefaultProfileDir() } = options;
|
const { markdownPath, submit = false, profileDir = getDefaultProfileDir() } = options;
|
||||||
|
|
||||||
@@ -83,28 +102,33 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
if (!chromePath) throw new Error('Chrome not found');
|
if (!chromePath) throw new Error('Chrome not found');
|
||||||
|
|
||||||
await mkdir(profileDir, { recursive: true });
|
await mkdir(profileDir, { recursive: true });
|
||||||
const port = await getFreePort();
|
const existingPort = await findExistingDebugPort(profileDir);
|
||||||
|
const port = existingPort ?? await getFreePort();
|
||||||
|
|
||||||
console.log(`[x-article] Launching Chrome...`);
|
if (existingPort) {
|
||||||
const chrome = spawn(chromePath, [
|
console.log(`[x-article] Reusing existing Chrome instance on port ${port}`);
|
||||||
`--remote-debugging-port=${port}`,
|
} else {
|
||||||
`--user-data-dir=${profileDir}`,
|
console.log(`[x-article] Launching Chrome...`);
|
||||||
'--no-first-run',
|
spawn(chromePath, [
|
||||||
'--no-default-browser-check',
|
`--remote-debugging-port=${port}`,
|
||||||
'--disable-blink-features=AutomationControlled',
|
`--user-data-dir=${profileDir}`,
|
||||||
'--start-maximized',
|
'--no-first-run',
|
||||||
X_ARTICLES_URL,
|
'--no-default-browser-check',
|
||||||
], { stdio: 'ignore' });
|
'--disable-blink-features=AutomationControlled',
|
||||||
|
'--start-maximized',
|
||||||
|
X_ARTICLES_URL,
|
||||||
|
], { stdio: 'ignore' });
|
||||||
|
}
|
||||||
|
|
||||||
let cdp: CdpConnection | null = null;
|
let cdp: CdpConnection | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const wsUrl = await waitForChromeDebugPort(port, 30_000);
|
const wsUrl = await waitForChromeDebugPort(port, 30_000, { includeLastError: true });
|
||||||
cdp = await CdpConnection.connect(wsUrl, 30_000, { defaultTimeoutMs: 30_000 });
|
cdp = await CdpConnection.connect(wsUrl, 30_000, { defaultTimeoutMs: 30_000 });
|
||||||
|
|
||||||
// Get page target
|
// Get page target
|
||||||
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
|
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
|
||||||
let pageTarget = targets.targetInfos.find((t) => t.type === 'page' && t.url.includes('x.com'));
|
let pageTarget = targets.targetInfos.find((t) => t.type === 'page' && t.url.startsWith(X_ARTICLES_URL));
|
||||||
|
|
||||||
if (!pageTarget) {
|
if (!pageTarget) {
|
||||||
const { targetId } = await cdp.send<{ targetId: string }>('Target.createTarget', { url: X_ARTICLES_URL });
|
const { targetId } = await cdp.send<{ targetId: string }>('Target.createTarget', { url: X_ARTICLES_URL });
|
||||||
@@ -409,15 +433,23 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
|
|
||||||
console.log('[x-article] Checking for placeholders in content...');
|
console.log('[x-article] Checking for placeholders in content...');
|
||||||
for (const img of parsed.contentImages) {
|
for (const img of parsed.contentImages) {
|
||||||
if (editorContent.result.value.includes(img.placeholder)) {
|
// Use regex for exact match (not followed by digit, e.g., XIMGPH_1 should not match XIMGPH_10)
|
||||||
|
const regex = new RegExp(img.placeholder + '(?!\\d)');
|
||||||
|
if (regex.test(editorContent.result.value)) {
|
||||||
console.log(`[x-article] Found: ${img.placeholder}`);
|
console.log(`[x-article] Found: ${img.placeholder}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[x-article] NOT found: ${img.placeholder}`);
|
console.log(`[x-article] NOT found: ${img.placeholder}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process images in sequential order (1, 2, 3, ...)
|
// Process images in XIMGPH order (1, 2, 3, ...) regardless of blockIndex
|
||||||
const sortedImages = [...parsed.contentImages].sort((a, b) => a.blockIndex - b.blockIndex);
|
const getPlaceholderIndex = (placeholder: string): number => {
|
||||||
|
const match = placeholder.match(/XIMGPH_(\d+)/);
|
||||||
|
return match ? Number(match[1]) : Number.POSITIVE_INFINITY;
|
||||||
|
};
|
||||||
|
const sortedImages = [...parsed.contentImages].sort(
|
||||||
|
(a, b) => getPlaceholderIndex(a.placeholder) - getPlaceholderIndex(b.placeholder),
|
||||||
|
);
|
||||||
|
|
||||||
for (let i = 0; i < sortedImages.length; i++) {
|
for (let i = 0; i < sortedImages.length; i++) {
|
||||||
const img = sortedImages[i]!;
|
const img = sortedImages[i]!;
|
||||||
@@ -440,22 +472,30 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
|
|
||||||
while ((node = walker.nextNode())) {
|
while ((node = walker.nextNode())) {
|
||||||
const text = node.textContent || '';
|
const text = node.textContent || '';
|
||||||
const idx = text.indexOf(placeholder);
|
let searchStart = 0;
|
||||||
if (idx !== -1) {
|
let idx;
|
||||||
// Found the placeholder - scroll to it first
|
// Search for exact match (not prefix of longer placeholder like XIMGPH_1 in XIMGPH_10)
|
||||||
const parentElement = node.parentElement;
|
while ((idx = text.indexOf(placeholder, searchStart)) !== -1) {
|
||||||
if (parentElement) {
|
const afterIdx = idx + placeholder.length;
|
||||||
parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
const charAfter = text[afterIdx];
|
||||||
}
|
// Exact match if next char is not a digit (XIMGPH_1 should not match XIMGPH_10)
|
||||||
|
if (charAfter === undefined || !/\\d/.test(charAfter)) {
|
||||||
|
// Found exact placeholder - scroll to it first
|
||||||
|
const parentElement = node.parentElement;
|
||||||
|
if (parentElement) {
|
||||||
|
parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
// Select it
|
// Select it
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.setStart(node, idx);
|
range.setStart(node, idx);
|
||||||
range.setEnd(node, idx + placeholder.length);
|
range.setEnd(node, idx + placeholder.length);
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
sel.addRange(range);
|
sel.addRange(range);
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
searchStart = afterIdx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -505,31 +545,54 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
// Wait for clipboard to be fully ready
|
// Wait for clipboard to be fully ready
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
|
|
||||||
// Delete placeholder by pressing Backspace (more reliable than Enter for replacing selection)
|
// Delete placeholder using execCommand (more reliable than keyboard events for DraftJS)
|
||||||
console.log(`[x-article] Deleting placeholder...`);
|
console.log(`[x-article] Deleting placeholder...`);
|
||||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId });
|
const deleteResult = await cdp.send<{ result: { value: boolean } }>('Runtime.evaluate', {
|
||||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId });
|
expression: `(() => {
|
||||||
|
const sel = window.getSelection();
|
||||||
|
if (!sel || sel.isCollapsed) return false;
|
||||||
|
// Try execCommand delete first
|
||||||
|
if (document.execCommand('delete', false)) return true;
|
||||||
|
// Fallback: replace selection with empty using insertText
|
||||||
|
document.execCommand('insertText', false, '');
|
||||||
|
return true;
|
||||||
|
})()`,
|
||||||
|
returnByValue: true,
|
||||||
|
}, { sessionId });
|
||||||
|
|
||||||
// Wait and verify placeholder is deleted
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
// Check that placeholder is no longer in editor
|
// Check that placeholder is no longer in editor (exact match, not substring)
|
||||||
const afterDelete = await cdp.send<{ result: { value: boolean } }>('Runtime.evaluate', {
|
const afterDelete = await cdp.send<{ result: { value: boolean } }>('Runtime.evaluate', {
|
||||||
expression: `(() => {
|
expression: `(() => {
|
||||||
const editor = document.querySelector('.DraftEditor-editorContainer [data-contents="true"]');
|
const editor = document.querySelector('.DraftEditor-editorContainer [data-contents="true"]');
|
||||||
if (!editor) return true;
|
if (!editor) return true;
|
||||||
return !editor.innerText.includes(${JSON.stringify(img.placeholder)});
|
const text = editor.innerText;
|
||||||
|
const placeholder = ${JSON.stringify(img.placeholder)};
|
||||||
|
// Use regex to find exact match (not followed by digit)
|
||||||
|
const regex = new RegExp(placeholder + '(?!\\\\d)');
|
||||||
|
return !regex.test(text);
|
||||||
})()`,
|
})()`,
|
||||||
returnByValue: true,
|
returnByValue: true,
|
||||||
}, { sessionId });
|
}, { sessionId });
|
||||||
|
|
||||||
if (!afterDelete.result.value) {
|
if (!afterDelete.result.value) {
|
||||||
console.warn(`[x-article] Placeholder may not have been deleted, trying again...`);
|
console.warn(`[x-article] Placeholder may not have been deleted, trying dispatchEvent...`);
|
||||||
// Try selecting and deleting again
|
// Try selecting and deleting with InputEvent
|
||||||
await selectPlaceholder(1);
|
await selectPlaceholder(1);
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId });
|
await cdp.send('Runtime.evaluate', {
|
||||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId });
|
expression: `(() => {
|
||||||
|
const editor = document.querySelector('.DraftEditor-editorContainer [contenteditable="true"]');
|
||||||
|
if (!editor) return;
|
||||||
|
editor.focus();
|
||||||
|
// Dispatch beforeinput and input events for deletion
|
||||||
|
const beforeEvent = new InputEvent('beforeinput', { inputType: 'deleteContentBackward', bubbles: true, cancelable: true });
|
||||||
|
editor.dispatchEvent(beforeEvent);
|
||||||
|
const inputEvent = new InputEvent('input', { inputType: 'deleteContentBackward', bubbles: true });
|
||||||
|
editor.dispatchEvent(inputEvent);
|
||||||
|
})()`,
|
||||||
|
}, { sessionId });
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user