mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 00:33:20 +08:00
merge: resolve v2.0.1 lib conflict by keeping our build
This commit is contained in:
commit
1ee03254aa
10 changed files with 259 additions and 220 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -6,6 +6,15 @@ This file records the notable changes in each dsh-im release. Its format follows
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.0.1] - 2026-08-24
|
||||
|
||||
### Fixed / 修复
|
||||
|
||||
- 机器人卡片的操作区域现在会在空间不足时自动换行,避免英文或其他较长本地化文案把操作按钮挤出可视区域。
|
||||
Bot-card actions now wrap when space is limited, preventing English or other longer localized labels from pushing actions out of view.
|
||||
- 一个 IM 渠道激活失败时,Host 现在会记录错误并继续依次激活其他渠道,避免单个渠道的本地配置或初始化故障阻断其余渠道。
|
||||
When one IM channel fails to activate, the Host now logs the error and continues activating the remaining channels in order, so a channel-local configuration or initialization failure cannot block the others.
|
||||
|
||||
## [2.0.0] - 2026-08-24
|
||||
|
||||
### Added / 新增
|
||||
|
|
@ -239,7 +248,8 @@ This file records the notable changes in each dsh-im release. Its format follows
|
|||
- 改进 npm 发布包结构,保留 CLI 入口并避免安装脚本拦截。
|
||||
Improved npm package contents to preserve the CLI entry point and avoid install-script blocking.
|
||||
|
||||
[Unreleased]: https://github.com/xmanrui/dsh-im/compare/v2.0.0...HEAD
|
||||
[Unreleased]: https://github.com/xmanrui/dsh-im/compare/v2.0.1...HEAD
|
||||
[2.0.1]: https://github.com/xmanrui/dsh-im/compare/v2.0.0...v2.0.1
|
||||
[2.0.0]: https://github.com/xmanrui/dsh-im/compare/v1.5.0...v2.0.0
|
||||
[1.5.0]: https://github.com/xmanrui/dsh-im/compare/v1.4.0...v1.5.0
|
||||
[1.4.0]: https://github.com/xmanrui/dsh-im/compare/v1.3.0...v1.4.0
|
||||
|
|
|
|||
|
|
@ -4196,7 +4196,7 @@ var CSS3 = String.raw`
|
|||
.bxf-connectedFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
|
||||
.bxf-healthSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
|
||||
.bxf-healthSummary[data-error="true"] { color: var(--bxf-error); }
|
||||
.bxf-botActions { flex: none; flex-wrap: nowrap; gap: 8px; margin-top: 0; justify-content: flex-end; }
|
||||
.bxf-botActions { flex: none; width: 100%; flex-wrap: wrap; gap: 8px; margin-top: 0; justify-content: flex-end; }
|
||||
.bxf-botActions .bxf-button { flex: none; white-space: nowrap; }
|
||||
.bxf-botActions .bxf-repairButton { color: var(--bxf-accent); border-color: color-mix(in srgb, var(--bxf-accent) 35%, var(--dsw-alias-border-l2, #dee0e3)); }
|
||||
.bxf-botActions .bxf-repairButton:hover:not(:disabled) { background: color-mix(in srgb, var(--bxf-accent) 7%, transparent); }
|
||||
|
|
@ -10222,9 +10222,9 @@ var CSS11 = String.raw`
|
|||
.dim-directoryPickerActions button:disabled { cursor: not-allowed; opacity: .52; }
|
||||
.dim-panel .dim-cardSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font: inherit; font-size: 12px; font-weight: 400; line-height: normal; }
|
||||
.dim-panel .dim-cardFooterLayout { min-width: 0; width: 100%; display: flex; flex-direction: column; align-items: stretch; gap: 9px; }
|
||||
.dim-panel .dim-cardFooterLayout > .dim-cardActions { align-self: flex-end; }
|
||||
.dim-panel .dim-cardFooterLayout > .dim-cardActions { align-self: stretch; }
|
||||
.dim-panel .dim-cardFeedback { width: 100%; padding: 8px 10px; border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-module-platform, #f7f8fa); font: inherit; font-size: 12px; font-weight: 400; line-height: 18px; overflow-wrap: anywhere; white-space: normal; }
|
||||
.dim-panel .dim-cardActions { flex: none; display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; margin: 0 0 0 auto; }
|
||||
.dim-panel .dim-cardActions { flex: none; width: 100%; display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 8px; margin: 0; }
|
||||
.dim-panel .dim-cardActions .dim-cardAction { flex: none; min-height: 32px; padding: 0 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 13px; font-weight: 560; line-height: normal; white-space: nowrap; }
|
||||
.dim-panel .dim-cardActions .dim-cardAction:hover:not(:disabled) { border-color: #aeb3bb; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
|
||||
.dim-panel .dim-cardActions .dim-cardAction[data-kind="danger"] { color: var(--dsw-alias-state-error-primary, #d54941); }
|
||||
|
|
|
|||
294
lib/index.js
294
lib/index.js
File diff suppressed because one or more lines are too long
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@xmanrui/dsh-im",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@xmanrui/dsh-im",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tencent-connect/qqbot-connector": "1.2.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@xmanrui/dsh-im",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
|
||||
"keywords": [
|
||||
"deepseek-harness",
|
||||
|
|
|
|||
|
|
@ -450,7 +450,7 @@ const CSS = String.raw`
|
|||
.bxf-connectedFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
|
||||
.bxf-healthSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
|
||||
.bxf-healthSummary[data-error="true"] { color: var(--bxf-error); }
|
||||
.bxf-botActions { flex: none; flex-wrap: nowrap; gap: 8px; margin-top: 0; justify-content: flex-end; }
|
||||
.bxf-botActions { flex: none; width: 100%; flex-wrap: wrap; gap: 8px; margin-top: 0; justify-content: flex-end; }
|
||||
.bxf-botActions .bxf-button { flex: none; white-space: nowrap; }
|
||||
.bxf-botActions .bxf-repairButton { color: var(--bxf-accent); border-color: color-mix(in srgb, var(--bxf-accent) 35%, var(--dsw-alias-border-l2, #dee0e3)); }
|
||||
.bxf-botActions .bxf-repairButton:hover:not(:disabled) { background: color-mix(in srgb, var(--bxf-accent) 7%, transparent); }
|
||||
|
|
|
|||
|
|
@ -206,9 +206,9 @@ const CSS = String.raw`
|
|||
.dim-directoryPickerActions button:disabled { cursor: not-allowed; opacity: .52; }
|
||||
.dim-panel .dim-cardSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font: inherit; font-size: 12px; font-weight: 400; line-height: normal; }
|
||||
.dim-panel .dim-cardFooterLayout { min-width: 0; width: 100%; display: flex; flex-direction: column; align-items: stretch; gap: 9px; }
|
||||
.dim-panel .dim-cardFooterLayout > .dim-cardActions { align-self: flex-end; }
|
||||
.dim-panel .dim-cardFooterLayout > .dim-cardActions { align-self: stretch; }
|
||||
.dim-panel .dim-cardFeedback { width: 100%; padding: 8px 10px; border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-module-platform, #f7f8fa); font: inherit; font-size: 12px; font-weight: 400; line-height: 18px; overflow-wrap: anywhere; white-space: normal; }
|
||||
.dim-panel .dim-cardActions { flex: none; display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; margin: 0 0 0 auto; }
|
||||
.dim-panel .dim-cardActions { flex: none; width: 100%; display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 8px; margin: 0; }
|
||||
.dim-panel .dim-cardActions .dim-cardAction { flex: none; min-height: 32px; padding: 0 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 13px; font-weight: 560; line-height: normal; white-space: nowrap; }
|
||||
.dim-panel .dim-cardActions .dim-cardAction:hover:not(:disabled) { border-color: #aeb3bb; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
|
||||
.dim-panel .dim-cardActions .dim-cardAction[data-kind="danger"] { color: var(--dsw-alias-state-error-primary, #d54941); }
|
||||
|
|
|
|||
|
|
@ -37,6 +37,18 @@ export function createImHostPlugin(internals = {}) {
|
|||
const startDiscord = internals.applyDiscord ?? applyDiscord;
|
||||
const startOffice = internals.applyOffice ?? applyOffice;
|
||||
const startWhatsapp = internals.applyWhatsapp ?? applyWhatsapp;
|
||||
const channels = [
|
||||
['feishu', startFeishu],
|
||||
['weixin', startWeixin],
|
||||
['dingtalk', startDingtalk],
|
||||
['wecom', startWecom],
|
||||
['qq', startQq],
|
||||
['slack', startSlack],
|
||||
['telegram', startTelegram],
|
||||
['discord', startDiscord],
|
||||
['whatsapp', startWhatsapp],
|
||||
['office', startOffice],
|
||||
];
|
||||
return Object.freeze({
|
||||
name,
|
||||
inject,
|
||||
|
|
@ -49,16 +61,21 @@ export function createImHostPlugin(internals = {}) {
|
|||
} else {
|
||||
installOutboundArtifactTool(ctx);
|
||||
}
|
||||
await startFeishu(ctx, channelConfig(config, 'feishu'));
|
||||
await startWeixin(ctx, channelConfig(config, 'weixin'));
|
||||
await startDingtalk(ctx, channelConfig(config, 'dingtalk'));
|
||||
await startWecom(ctx, channelConfig(config, 'wecom'));
|
||||
await startQq(ctx, channelConfig(config, 'qq'));
|
||||
await startSlack(ctx, channelConfig(config, 'slack'));
|
||||
await startTelegram(ctx, channelConfig(config, 'telegram'));
|
||||
await startDiscord(ctx, channelConfig(config, 'discord'));
|
||||
await startWhatsapp(ctx, channelConfig(config, 'whatsapp'));
|
||||
await startOffice(ctx, channelConfig(config, 'office'));
|
||||
const logger = typeof ctx?.logger === 'function'
|
||||
? ctx.logger(name)
|
||||
: (ctx?.logger ?? console);
|
||||
const failures = [];
|
||||
for (const [channel, start] of channels) {
|
||||
try {
|
||||
await start(ctx, channelConfig(config, channel));
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
logger.error?.(`[dsh-im] failed to activate ${channel}; continuing with the remaining channels`, error);
|
||||
}
|
||||
}
|
||||
if (failures.length === channels.length) {
|
||||
throw new AggregateError(failures, 'dsh-im failed to activate every channel');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -510,7 +510,7 @@ test('DingTalk bot cards omit the redundant received and replied metric', () =>
|
|||
assert.doesNotMatch(markup, /收到 \/ 回复/);
|
||||
});
|
||||
|
||||
test('all IM channel cards place one-row actions above full-width feedback', async () => {
|
||||
test('all IM channel cards keep localized actions visible above full-width feedback', async () => {
|
||||
const [imStyles, feishuStyles, weixinStyles, dingtalkStyles] = await Promise.all([
|
||||
readFile(STYLES_URL, 'utf8'),
|
||||
readFile(FEISHU_STYLES_URL, 'utf8'),
|
||||
|
|
@ -518,12 +518,13 @@ test('all IM channel cards place one-row actions above full-width feedback', asy
|
|||
readFile(DINGTALK_STYLES_URL, 'utf8'),
|
||||
]);
|
||||
|
||||
assert.match(feishuStyles, /\.bxf-botActions \{[^}]*flex-wrap: nowrap;/);
|
||||
assert.match(feishuStyles, /\.bxf-botActions \{[^}]*width: 100%;[^}]*flex-wrap: wrap;/);
|
||||
assert.match(weixinStyles, /\.dxw-accountFooter \.dxw-actions \{[^}]*flex-wrap: nowrap;/);
|
||||
assert.match(dingtalkStyles, /\.ddt-accountFooter \.ddt-actions \{[^}]*flex-wrap: nowrap;/);
|
||||
assert.match(imStyles, /\.dim-panel \.dim-cardFooter \{[^}]*gap: 12px;[^}]*padding-top: 6px;[^}]*border-top: 1px solid/);
|
||||
assert.match(imStyles, /\.dim-panel \.dim-cardFooterLayout \{[^}]*width: 100%;[^}]*flex-direction: column;[^}]*align-items: stretch;/);
|
||||
assert.match(imStyles, /\.dim-panel \.dim-cardFooterLayout > \.dim-cardActions \{[^}]*align-self: flex-end;/);
|
||||
assert.match(imStyles, /\.dim-panel \.dim-cardFooterLayout > \.dim-cardActions \{[^}]*align-self: stretch;/);
|
||||
assert.match(imStyles, /\.dim-panel \.dim-cardActions \{[^}]*width: 100%;[^}]*justify-content: flex-end;[^}]*flex-wrap: wrap;/);
|
||||
assert.match(imStyles, /\.dim-panel \.dim-cardFeedback \{[^}]*width: 100%;[^}]*overflow-wrap: anywhere;[^}]*white-space: normal;/);
|
||||
assert.match(imStyles, /\.dim-panel \.dim-cardActions \.dim-cardAction \{[^}]*min-height: 32px;[^}]*border-radius: 8px;[^}]*font-size: 13px;/);
|
||||
assert.match(imStyles, /\.dim-panel \.dim-cardActions \.dim-cardAction\[data-kind="danger"\] \{[^}]*#d54941/);
|
||||
|
|
|
|||
|
|
@ -55,58 +55,69 @@ test('Host composes nine IM channels and the AI Office connector inside one plug
|
|||
]);
|
||||
});
|
||||
|
||||
test('Host does not start Weixin when Feishu activation fails', async () => {
|
||||
let weixinStarted = false;
|
||||
const plugin = createImHostPlugin({
|
||||
applyFeishu: async () => { throw new Error('feishu unavailable'); },
|
||||
applyWeixin: async () => { weixinStarted = true; },
|
||||
applyDingtalk: async () => { throw new Error('DingTalk must not start'); },
|
||||
applyWecom: async () => { throw new Error('Enterprise WeChat must not start'); },
|
||||
applyQq: async () => { throw new Error('QQ must not start'); },
|
||||
});
|
||||
const CHANNELS = [
|
||||
['feishu', 'applyFeishu'],
|
||||
['weixin', 'applyWeixin'],
|
||||
['dingtalk', 'applyDingtalk'],
|
||||
['wecom', 'applyWecom'],
|
||||
['qq', 'applyQq'],
|
||||
['slack', 'applySlack'],
|
||||
['telegram', 'applyTelegram'],
|
||||
['discord', 'applyDiscord'],
|
||||
['whatsapp', 'applyWhatsapp'],
|
||||
['office', 'applyOffice'],
|
||||
];
|
||||
|
||||
await assert.rejects(() => plugin.apply({}, {}), /feishu unavailable/);
|
||||
assert.equal(weixinStarted, false);
|
||||
function activationFixture(failedChannels) {
|
||||
const calls = [];
|
||||
const events = [];
|
||||
const errors = [];
|
||||
const failures = new Map();
|
||||
const internals = Object.fromEntries(CHANNELS.map(([channel, applyName]) => [
|
||||
applyName,
|
||||
async () => {
|
||||
calls.push(channel);
|
||||
events.push(`${channel}:start`);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
if (failedChannels.has(channel)) {
|
||||
const error = new Error(`${channel} unavailable`);
|
||||
failures.set(channel, error);
|
||||
events.push(`${channel}:failed`);
|
||||
throw error;
|
||||
}
|
||||
events.push(`${channel}:end`);
|
||||
},
|
||||
]));
|
||||
const ctx = { logger: { error: (...args) => errors.push(args) } };
|
||||
return { plugin: createImHostPlugin(internals), ctx, calls, events, errors, failures };
|
||||
}
|
||||
|
||||
test('Host continues activating channels in order when one channel fails', async () => {
|
||||
for (const [failedChannel] of CHANNELS) {
|
||||
const fixture = activationFixture(new Set([failedChannel]));
|
||||
|
||||
await fixture.plugin.apply(fixture.ctx, {});
|
||||
|
||||
assert.deepEqual(fixture.calls, CHANNELS.map(([channel]) => channel));
|
||||
assert.deepEqual(fixture.events, CHANNELS.flatMap(([channel]) => [
|
||||
`${channel}:start`,
|
||||
`${channel}:${channel === failedChannel ? 'failed' : 'end'}`,
|
||||
]));
|
||||
assert.equal(fixture.errors.length, 1);
|
||||
assert.match(fixture.errors[0][0], new RegExp(`activate ${failedChannel}`));
|
||||
assert.equal(fixture.errors[0][1], fixture.failures.get(failedChannel));
|
||||
}
|
||||
});
|
||||
|
||||
test('Host does not start DingTalk when Weixin activation fails', async () => {
|
||||
let dingtalkStarted = false;
|
||||
const plugin = createImHostPlugin({
|
||||
applyFeishu: async () => {},
|
||||
applyWeixin: async () => { throw new Error('weixin unavailable'); },
|
||||
applyDingtalk: async () => { dingtalkStarted = true; },
|
||||
applyWecom: async () => { throw new Error('Enterprise WeChat must not start'); },
|
||||
applyQq: async () => { throw new Error('QQ must not start'); },
|
||||
});
|
||||
test('Host reports aggregate failure only after every channel was attempted', async () => {
|
||||
const fixture = activationFixture(new Set(CHANNELS.map(([channel]) => channel)));
|
||||
|
||||
await assert.rejects(() => plugin.apply({}, {}), /weixin unavailable/);
|
||||
assert.equal(dingtalkStarted, false);
|
||||
});
|
||||
|
||||
test('Host does not start Enterprise WeChat when DingTalk activation fails', async () => {
|
||||
let wecomStarted = false;
|
||||
const plugin = createImHostPlugin({
|
||||
applyFeishu: async () => {},
|
||||
applyWeixin: async () => {},
|
||||
applyDingtalk: async () => { throw new Error('dingtalk unavailable'); },
|
||||
applyWecom: async () => { wecomStarted = true; },
|
||||
applyQq: async () => { throw new Error('QQ must not start'); },
|
||||
});
|
||||
|
||||
await assert.rejects(() => plugin.apply({}, {}), /dingtalk unavailable/);
|
||||
assert.equal(wecomStarted, false);
|
||||
});
|
||||
|
||||
test('Host does not start QQ when Enterprise WeChat activation fails', async () => {
|
||||
let qqStarted = false;
|
||||
const plugin = createImHostPlugin({
|
||||
applyFeishu: async () => {},
|
||||
applyWeixin: async () => {},
|
||||
applyDingtalk: async () => {},
|
||||
applyWecom: async () => { throw new Error('wecom unavailable'); },
|
||||
applyQq: async () => { qqStarted = true; },
|
||||
});
|
||||
|
||||
await assert.rejects(() => plugin.apply({}, {}), /wecom unavailable/);
|
||||
assert.equal(qqStarted, false);
|
||||
await assert.rejects(
|
||||
() => fixture.plugin.apply(fixture.ctx, {}),
|
||||
(error) => error instanceof AggregateError
|
||||
&& error.errors.length === CHANNELS.length
|
||||
&& /failed to activate every channel/.test(error.message),
|
||||
);
|
||||
assert.deepEqual(fixture.calls, CHANNELS.map(([channel]) => channel));
|
||||
assert.equal(fixture.errors.length, CHANNELS.length);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue