koishi-plugin-ll-douyin 0.1.0 → 0.1.2

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/index.js CHANGED
@@ -1 +1 @@
1
- import{defineComponent as ee,ref as i,onMounted as le,resolveComponent as d,openBlock as r,createBlock as $,withCtx as a,createVNode as s,createElementVNode as n,withKeys as te,createTextVNode as u,createElementBlock as _,Fragment as E,renderList as I,toDisplayString as o,createCommentVNode as T}from"vue";import{send as ae}from"@koishijs/client";const se={class:"head"},ne={class:"row"},oe={key:0,class:"results"},ue={class:"meta"},ie={class:"name"},de={class:"sub"},ce={key:1,class:"hint pad"},re={class:"row"},ve={class:"hint"},me={class:"card-head"},fe={class:"hint"},_e={key:0,class:"notice"},pe=ee({__name:"page",setup(k){const g=i(""),p=i(false),w=i(false),y=i([]),C=i([]),U=i(""),z=i([]),m=i(""),B=i([]),N=i(false),V=i(""),v=i({accounts:0,subs:0,checks:0,pushes:0,lastCheck:"",lastError:"",cookie:false,cookieOk:false}),f=(l,...e)=>ae(l,...e);function j(l){return l?l>=1e4?(l/1e4).toFixed(1)+" 万":String(l):"0"}function c(l){V.value=l,setTimeout(()=>{V.value===l&&(V.value="")},4e3)}async function K(){const l=g.value.trim();if(l){p.value=true,w.value=false;try{y.value=await f("ll-douyin/search",l)}catch(e){y.value=[],c("搜索失败:"+((e==null?void 0:e.message)||e))}finally{p.value=false,w.value=true}}}async function D(){try{C.value=await f("ll-douyin/bots"),!U.value&&C.value.length&&(U.value=C.value[0].key,await O())}catch(l){c("读取机器人失败:"+((l==null?void 0:l.message)||l))}}async function O(){const l=C.value.find(e=>e.key===U.value);if(l)try{z.value=await f("ll-douyin/groups",l.platform,l.selfId),z.value.some(e=>e.umo===m.value)||(m.value="")}catch(e){z.value=[],c("拉群列表失败:"+((e==null?void 0:e.message)||e))}}async function S(){try{B.value=await f("ll-douyin/list")}catch(l){c("读取订阅失败:"+((l==null?void 0:l.message)||l))}}async function P(){try{v.value=await f("ll-douyin/state")}catch{}}async function q(){N.value=true;try{await Promise.all([D(),S(),P()])}finally{N.value=false}}async function G(l){if(!m.value)return c("先在②里选一个群");try{const e=await f("ll-douyin/subscribe",{umo:m.value,secUid:l.secUid});c(e!=null&&e.added?`已订阅:${l.nickname||l.secUid}`:"这个群已经订过该账号"),await S()}catch(e){c("订阅失败:"+((e==null?void 0:e.message)||e))}}async function H(l){if(confirm(`取消订阅「${l.nickname||l.secUid}」?`))try{await f("ll-douyin/unsubscribe",{umo:l.umo,secUid:l.secUid}),await S()}catch(e){c("取消失败:"+((e==null?void 0:e.message)||e))}}async function J(l){l._testing=true;try{const e=await f("ll-douyin/test",{umo:l.umo,secUid:l.secUid});c((e==null?void 0:e.message)||"测试推送已发送"),await P()}catch(e){c("测试推送失败:"+((e==null?void 0:e.message)||e))}finally{l._testing=false}}return le(q),(l,e)=>{const x=d("el-button"),Q=d("el-input"),R=d("el-avatar"),A=d("el-card"),F=d("el-option"),L=d("el-select"),h=d("el-table-column"),W=d("el-tag"),X=d("el-table"),b=d("el-descriptions-item"),Y=d("el-descriptions"),Z=d("k-layout");return r(),$(Z,null,{header:a(()=>[n("div",se,[e[4]||(e[4]=n("span",{class:"title"},"抖音订阅推送",-1)),s(x,{size:"small",loading:N.value,onClick:q},{default:a(()=>[...e[3]||(e[3]=[u("刷新",-1)])]),_:1},8,["loading"])])]),default:a(()=>[s(A,{class:"card",shadow:"never"},{header:a(()=>[...e[5]||(e[5]=[n("div",{class:"card-head"},[n("b",null,"① 搜索账号"),n("span",{class:"hint"},"主页链接 / 抖音号 / 昵称都可以,链接成功率最高")],-1)])]),default:a(()=>[n("div",ne,[s(Q,{modelValue:g.value,"onUpdate:modelValue":e[0]||(e[0]=t=>g.value=t),placeholder:"https://www.douyin.com/user/MS4wLjABAAAA... 或 抖音号",clearable:"",onKeyup:te(K,["enter"])},null,8,["modelValue"]),s(x,{type:"primary",loading:p.value,onClick:K},{default:a(()=>[...e[6]||(e[6]=[u("搜索账号",-1)])]),_:1},8,["loading"])]),y.value.length?(r(),_("div",oe,[(r(true),_(E,null,I(y.value,t=>(r(),_("div",{key:t.secUid,class:"item"},[s(R,{src:t.avatar,size:42},{default:a(()=>[u(o((t.nickname||"?").slice(0,1)),1)]),_:2},1032,["src"]),n("div",ue,[n("div",ie,o(t.nickname||"(未命名)"),1),n("div",de,o(t.uniqueId||t.secUid)+" · 粉丝 "+o(j(t.followerCount)),1)]),s(x,{size:"small",type:"primary",disabled:!m.value,onClick:M=>G(t)},{default:a(()=>[...e[7]||(e[7]=[u(" 订阅到此目标 ",-1)])]),_:1},8,["disabled","onClick"])]))),128))])):w.value?(r(),_("div",ce,"没搜到。抖音站内搜索对高频请求会返回空结果,建议直接粘主页链接。")):T("v-if",true)]),_:1}),s(A,{class:"card",shadow:"never"},{header:a(()=>[...e[8]||(e[8]=[n("div",{class:"card-head"},[n("b",null,"② 推送目标"),n("span",{class:"hint"},"先选机器人,再选群")],-1)])]),default:a(()=>[n("div",re,[s(L,{modelValue:U.value,"onUpdate:modelValue":e[1]||(e[1]=t=>U.value=t),placeholder:"选择机器人",style:{width:"200px"},onChange:O},{default:a(()=>[(r(true),_(E,null,I(C.value,t=>(r(),$(F,{key:t.key,label:t.label,value:t.key},null,8,["label","value"]))),128))]),_:1},8,["modelValue"]),s(L,{modelValue:m.value,"onUpdate:modelValue":e[2]||(e[2]=t=>m.value=t),placeholder:"选择群 / 频道",filterable:"",style:{width:"280px"}},{default:a(()=>[(r(true),_(E,null,I(z.value,t=>(r(),$(F,{key:t.umo,label:t.name,value:t.umo},null,8,["label","value"]))),128))]),_:1},8,["modelValue"]),n("span",ve,"当前:"+o(m.value||"未选择"),1)])]),_:1}),s(A,{class:"card",shadow:"never"},{header:a(()=>[n("div",me,[e[9]||(e[9]=n("b",null,"③ 已订阅",-1)),n("span",fe,"共 "+o(B.value.length)+" 条",1)])]),default:a(()=>[s(X,{data:B.value,size:"small","empty-text":"还没有订阅"},{default:a(()=>[s(h,{prop:"targetName",label:"推送目标","min-width":"170","show-overflow-tooltip":""}),s(h,{label:"账号","min-width":"160","show-overflow-tooltip":""},{default:a(({row:t})=>[u(o(t.nickname||t.secUid),1)]),_:1}),s(h,{prop:"uniqueId",label:"抖音号",width:"140","show-overflow-tooltip":""}),s(h,{prop:"seen",label:"已记录",width:"86"}),s(h,{label:"状态",width:"92"},{default:a(({row:t})=>[s(W,{type:t.failCount?"danger":"success",size:"small"},{default:a(()=>[u(o(t.failCount?"失败 "+t.failCount:"正常"),1)]),_:2},1032,["type"])]),_:1}),s(h,{label:"操作",width:"190"},{default:a(({row:t})=>[s(x,{size:"small",loading:t._testing,onClick:M=>J(t)},{default:a(()=>[...e[10]||(e[10]=[u("测试推送",-1)])]),_:1},8,["loading","onClick"]),s(x,{size:"small",type:"danger",plain:"",onClick:M=>H(t)},{default:a(()=>[...e[11]||(e[11]=[u("取消",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])]),_:1}),s(A,{class:"card",shadow:"never"},{header:a(()=>[...e[12]||(e[12]=[n("div",{class:"card-head"},[n("b",null,"④ 运行状态")],-1)])]),default:a(()=>[s(Y,{column:3,size:"small",border:""},{default:a(()=>[s(b,{label:"Cookie"},{default:a(()=>[u(o(v.value.cookieOk?"有效":v.value.cookie?"未验证":"未配置"),1)]),_:1}),s(b,{label:"账号 / 订阅"},{default:a(()=>[u(o(v.value.accounts)+" / "+o(v.value.subs),1)]),_:1}),s(b,{label:"最近检查"},{default:a(()=>[u(o(v.value.lastCheck||"—"),1)]),_:1}),s(b,{label:"累计检查"},{default:a(()=>[u(o(v.value.checks),1)]),_:1}),s(b,{label:"累计推送"},{default:a(()=>[u(o(v.value.pushes),1)]),_:1}),s(b,{label:"最近错误"},{default:a(()=>[u(o(v.value.lastError||"—"),1)]),_:1})]),_:1})]),_:1}),V.value?(r(),_("div",_e,o(V.value),1)):T("v-if",true)]),_:1})}}}),ye=(k,g)=>{const p=k.__vccOpts||k;for(const[w,y]of g)p[w]=y;return p},he=ye(pe,[["__scopeId","data-v-ca67fa33"]]),ge=k=>{k.page({id:"ll-douyin",path:"/ll-douyin",name:"抖音订阅",desc:"订阅抖音账号,新作品自动推送",icon:"activity:default",order:200,component:he})};export{ge as default};
1
+ import{defineComponent as ee,ref as i,onMounted as le,resolveComponent as d,openBlock as c,createBlock as $,withCtx as a,createVNode as s,createElementVNode as n,withKeys as te,createTextVNode as u,createElementBlock as _,Fragment as E,renderList as I,toDisplayString as o,createCommentVNode as K}from"vue";import{send as ae}from"@koishijs/client";const se={class:"head"},ne={class:"row"},oe={key:0,class:"results"},ue={class:"meta"},ie={class:"name"},de={class:"sub"},ce={key:1,class:"hint pad"},re={class:"row"},ve={class:"hint"},me={key:0,class:"hint pad"},fe={class:"card-head"},_e={class:"hint"},pe={key:0,class:"notice"},ye=ee({__name:"page",setup(g){const w=i(""),p=i(false),C=i(false),y=i([]),U=i([]),h=i(""),V=i([]),m=i(""),B=i([]),N=i(false),x=i(""),v=i({accounts:0,subs:0,checks:0,pushes:0,lastCheck:"",lastError:"",cookie:false,cookieOk:false}),f=(l,...e)=>ae(l,...e);function T(l){return l?l>=1e4?(l/1e4).toFixed(1)+" 万":String(l):"0"}function r(l){x.value=l,setTimeout(()=>{x.value===l&&(x.value="")},4e3)}async function O(){const l=w.value.trim();if(l){p.value=true,C.value=false;try{y.value=await f("ll-douyin/search",l)}catch(e){y.value=[],r("搜索失败:"+((e==null?void 0:e.message)||e))}finally{p.value=false,C.value=true}}}async function j(){try{U.value=await f("ll-douyin/bots"),!h.value&&U.value.length&&(h.value=U.value[0].key,await P())}catch(l){r("读取机器人失败:"+((l==null?void 0:l.message)||l))}}async function P(){const l=U.value.find(e=>e.key===h.value);if(l)try{V.value=await f("ll-douyin/groups",l.platform,l.selfId),V.value.some(e=>e.umo===m.value)||(m.value="")}catch(e){V.value=[],r("拉群列表失败:"+((e==null?void 0:e.message)||e))}}async function S(){try{B.value=await f("ll-douyin/list")}catch(l){r("读取订阅失败:"+((l==null?void 0:l.message)||l))}}async function q(){try{v.value=await f("ll-douyin/state")}catch{}}async function F(){N.value=true;try{await Promise.all([j(),S(),q()])}finally{N.value=false}}async function D(l){if(!m.value)return r("先在②里选一个群");try{const e=await f("ll-douyin/subscribe",{umo:m.value,secUid:l.secUid});r(e!=null&&e.added?`已订阅:${l.nickname||l.secUid}`:"这个群已经订过该账号"),await S()}catch(e){r("订阅失败:"+((e==null?void 0:e.message)||e))}}async function G(l){if(confirm(`取消订阅「${l.nickname||l.secUid}」?`))try{await f("ll-douyin/unsubscribe",{umo:l.umo,secUid:l.secUid}),await S()}catch(e){r("取消失败:"+((e==null?void 0:e.message)||e))}}async function H(l){l._testing=true;try{const e=await f("ll-douyin/test",{umo:l.umo,secUid:l.secUid});r((e==null?void 0:e.message)||"测试推送已发送"),await q()}catch(e){r("测试推送失败:"+((e==null?void 0:e.message)||e))}finally{l._testing=false}}return le(F),(l,e)=>{const z=d("el-button"),J=d("el-input"),R=d("el-avatar"),A=d("el-card"),L=d("el-option"),M=d("el-select"),b=d("el-table-column"),W=d("el-tag"),X=d("el-table"),k=d("el-descriptions-item"),Y=d("el-descriptions"),Z=d("k-layout");return c(),$(Z,null,{header:a(()=>[n("div",se,[e[4]||(e[4]=n("span",{class:"title"},"抖音订阅推送",-1)),s(z,{size:"small",loading:N.value,onClick:F},{default:a(()=>[...e[3]||(e[3]=[u("刷新",-1)])]),_:1},8,["loading"])])]),default:a(()=>[s(A,{class:"card",shadow:"never"},{header:a(()=>[...e[5]||(e[5]=[n("div",{class:"card-head"},[n("b",null,"① 搜索账号"),n("span",{class:"hint"},"主页链接 / 抖音号 / 昵称都可以,链接成功率最高")],-1)])]),default:a(()=>[n("div",ne,[s(J,{modelValue:w.value,"onUpdate:modelValue":e[0]||(e[0]=t=>w.value=t),placeholder:"https://www.douyin.com/user/MS4wLjABAAAA... 或 抖音号",clearable:"",onKeyup:te(O,["enter"])},null,8,["modelValue"]),s(z,{type:"primary",loading:p.value,onClick:O},{default:a(()=>[...e[6]||(e[6]=[u("搜索账号",-1)])]),_:1},8,["loading"])]),y.value.length?(c(),_("div",oe,[(c(true),_(E,null,I(y.value,t=>(c(),_("div",{key:t.secUid,class:"item"},[s(R,{src:t.avatar,size:42},{default:a(()=>[u(o((t.nickname||"?").slice(0,1)),1)]),_:2},1032,["src"]),n("div",ue,[n("div",ie,o(t.nickname||"(未命名)"),1),n("div",de,o(t.uniqueId||t.secUid)+" · 粉丝 "+o(T(t.followerCount)),1)]),s(z,{size:"small",type:"primary",disabled:!m.value,onClick:Q=>D(t)},{default:a(()=>[...e[7]||(e[7]=[u(" 订阅到此目标 ",-1)])]),_:1},8,["disabled","onClick"])]))),128))])):C.value?(c(),_("div",ce,"没搜到。抖音站内搜索对高频请求会返回空结果,建议直接粘主页链接。")):K("v-if",true)]),_:1}),s(A,{class:"card",shadow:"never"},{header:a(()=>[...e[8]||(e[8]=[n("div",{class:"card-head"},[n("b",null,"② 推送目标"),n("span",{class:"hint"},"先选机器人,再选群")],-1)])]),default:a(()=>[n("div",re,[s(M,{modelValue:h.value,"onUpdate:modelValue":e[1]||(e[1]=t=>h.value=t),placeholder:"选择机器人",style:{width:"200px"},onChange:P},{default:a(()=>[(c(true),_(E,null,I(U.value,t=>(c(),$(L,{key:t.key,label:t.label,value:t.key},null,8,["label","value"]))),128))]),_:1},8,["modelValue"]),s(M,{modelValue:m.value,"onUpdate:modelValue":e[2]||(e[2]=t=>m.value=t),placeholder:"选择群 / 频道",filterable:"",style:{width:"280px"}},{default:a(()=>[(c(true),_(E,null,I(V.value,t=>(c(),$(L,{key:t.umo,label:t.name,value:t.umo},null,8,["label","value"]))),128))]),_:1},8,["modelValue"]),n("span",ve,"当前:"+o(m.value||"未选择"),1)]),h.value&&!V.value.length?(c(),_("div",me," 官方 QQ 没有「列出群」的接口,群列表只能靠机器人收到过消息来积累:在群里 @ 一下机器人(或发条指令), 再点右上角刷新,那个群就会出现。 ")):K("v-if",true)]),_:1}),s(A,{class:"card",shadow:"never"},{header:a(()=>[n("div",fe,[e[9]||(e[9]=n("b",null,"③ 已订阅",-1)),n("span",_e,"共 "+o(B.value.length)+" 条",1)])]),default:a(()=>[s(X,{data:B.value,size:"small","empty-text":"还没有订阅"},{default:a(()=>[s(b,{prop:"targetName",label:"推送目标","min-width":"170","show-overflow-tooltip":""}),s(b,{label:"账号","min-width":"160","show-overflow-tooltip":""},{default:a(({row:t})=>[u(o(t.nickname||t.secUid),1)]),_:1}),s(b,{prop:"uniqueId",label:"抖音号",width:"140","show-overflow-tooltip":""}),s(b,{prop:"seen",label:"已记录",width:"86"}),s(b,{label:"状态",width:"92"},{default:a(({row:t})=>[s(W,{type:t.failCount?"danger":"success",size:"small"},{default:a(()=>[u(o(t.failCount?"失败 "+t.failCount:"正常"),1)]),_:2},1032,["type"])]),_:1}),s(b,{label:"操作",width:"190"},{default:a(({row:t})=>[s(z,{size:"small",loading:t._testing,onClick:Q=>H(t)},{default:a(()=>[...e[10]||(e[10]=[u("测试推送",-1)])]),_:1},8,["loading","onClick"]),s(z,{size:"small",type:"danger",plain:"",onClick:Q=>G(t)},{default:a(()=>[...e[11]||(e[11]=[u("取消",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])]),_:1}),s(A,{class:"card",shadow:"never"},{header:a(()=>[...e[12]||(e[12]=[n("div",{class:"card-head"},[n("b",null,"④ 运行状态")],-1)])]),default:a(()=>[s(Y,{column:3,size:"small",border:""},{default:a(()=>[s(k,{label:"Cookie"},{default:a(()=>[u(o(v.value.cookieOk?"有效":v.value.cookie?"未验证":"未配置"),1)]),_:1}),s(k,{label:"账号 / 订阅"},{default:a(()=>[u(o(v.value.accounts)+" / "+o(v.value.subs),1)]),_:1}),s(k,{label:"最近检查"},{default:a(()=>[u(o(v.value.lastCheck||"—"),1)]),_:1}),s(k,{label:"累计检查"},{default:a(()=>[u(o(v.value.checks),1)]),_:1}),s(k,{label:"累计推送"},{default:a(()=>[u(o(v.value.pushes),1)]),_:1}),s(k,{label:"最近错误"},{default:a(()=>[u(o(v.value.lastError||"—"),1)]),_:1})]),_:1})]),_:1}),x.value?(c(),_("div",pe,o(x.value),1)):K("v-if",true)]),_:1})}}}),he=(g,w)=>{const p=g.__vccOpts||g;for(const[C,y]of w)p[C]=y;return p},be=he(ye,[["__scopeId","data-v-75e9c86b"]]),we=g=>{g.page({id:"ll-douyin",path:"/ll-douyin",name:"抖音订阅",desc:"订阅抖音账号,新作品自动推送",icon:"activity:default",order:200,component:be})};export{we as default};
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- .head[data-v-ca67fa33]{display:flex;align-items:center;gap:12px}.title[data-v-ca67fa33]{font-size:18px;font-weight:600}.card[data-v-ca67fa33]{margin-bottom:14px}.card-head[data-v-ca67fa33]{display:flex;align-items:baseline;gap:10px}.hint[data-v-ca67fa33]{color:var(--k-text-secondary, #8a9099);font-size:12px}.hint.pad[data-v-ca67fa33]{padding:8px 0 2px}.row[data-v-ca67fa33]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.results[data-v-ca67fa33]{margin-top:12px;display:flex;flex-direction:column;gap:8px}.item[data-v-ca67fa33]{display:flex;align-items:center;gap:12px;padding:8px 10px;border:1px solid var(--k-border-color, #e3e5e9);border-radius:8px}.meta[data-v-ca67fa33]{flex:1;min-width:0}.name[data-v-ca67fa33]{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sub[data-v-ca67fa33]{color:var(--k-text-secondary, #8a9099);font-size:12px}.notice[data-v-ca67fa33]{position:sticky;bottom:0;margin-top:4px;padding:8px 12px;background:var(--k-color-primary-2, #eaf0ff);border-radius:6px;font-size:13px}
1
+ .head[data-v-75e9c86b]{display:flex;align-items:center;gap:12px}.title[data-v-75e9c86b]{font-size:18px;font-weight:600}.card[data-v-75e9c86b]{margin-bottom:14px}.card-head[data-v-75e9c86b]{display:flex;align-items:baseline;gap:10px}.hint[data-v-75e9c86b]{color:var(--k-text-secondary, #8a9099);font-size:12px}.hint.pad[data-v-75e9c86b]{padding:8px 0 2px}.row[data-v-75e9c86b]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.results[data-v-75e9c86b]{margin-top:12px;display:flex;flex-direction:column;gap:8px}.item[data-v-75e9c86b]{display:flex;align-items:center;gap:12px;padding:8px 10px;border:1px solid var(--k-border-color, #e3e5e9);border-radius:8px}.meta[data-v-75e9c86b]{flex:1;min-width:0}.name[data-v-75e9c86b]{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sub[data-v-75e9c86b]{color:var(--k-text-secondary, #8a9099);font-size:12px}.notice[data-v-75e9c86b]{position:sticky;bottom:0;margin-top:4px;padding:8px 12px;background:var(--k-color-primary-2, #eaf0ff);border-radius:6px;font-size:13px}
package/lib/index.js CHANGED
@@ -42,7 +42,9 @@ exports.Config = Schema.object({
42
42
  }).description('推送内容'),
43
43
 
44
44
  video: Schema.object({
45
- mode: Schema.union(['video', 'file']).description('视频投递方式:video = 视频消息,file = 群文件').default('video'),
45
+ mode: Schema.union(['video', 'file', 'link'])
46
+ .description('视频投递方式:video = 视频消息,file = 群文件,link = 只发封面 + 链接(最省流量,不会被视频审核卡)')
47
+ .default('video'),
46
48
  delivery: Schema.union(['auto', 'direct', 'download']).description('直链投递 / 先下载再投递').default('auto'),
47
49
  sizeLimitMb: Schema.number().description('体积上限(MB),超过则改发链接').min(0).default(100),
48
50
  coverOnFallback: Schema.boolean().description('降级发链接时附带封面').default(true),
@@ -171,6 +173,17 @@ function apply(ctx, config) {
171
173
  return `data:${mime};base64,${body.toString('base64')}`
172
174
  }
173
175
 
176
+ /** 封面图元素,需要本地化时转 data URI,失败退回直链。 */
177
+ async function coverEl(url, localize) {
178
+ if (!url) return null
179
+ if (!localize) return h.image(url)
180
+ try {
181
+ return h.image(await asDataUri(url, 20 * MB, 'image/jpeg'))
182
+ } catch (e) {
183
+ return h.image(url)
184
+ }
185
+ }
186
+
174
187
  async function buildPlan(post, platform) {
175
188
  const opt = config.content
176
189
  const lines = []
@@ -205,12 +218,17 @@ function apply(ctx, config) {
205
218
  const url = config.video.preferNoWatermark
206
219
  ? postNoWatermarkUrl(post) || post.videoUrl
207
220
  : post.videoUrl || postNoWatermarkUrl(post)
221
+ const limit = Math.max(config.video.sizeLimitMb, 0) * MB
208
222
 
209
- if (!url) {
223
+ // mode=link:不发视频本体,只发「封面 + 链接」
224
+ if (config.video.mode === 'link') {
225
+ lines.push(`🔗 ${link}`)
226
+ if (config.video.coverOnFallback && post.coverUrl) media.push(await coverEl(post.coverUrl, localize))
227
+ kind = 'link'
228
+ } else if (!url) {
210
229
  lines.push(`🔗 ${link}`)
211
230
  kind = 'link'
212
231
  } else {
213
- const limit = Math.max(config.video.sizeLimitMb, 0) * MB
214
232
  let size = 0
215
233
  if (limit > 0) {
216
234
  try {
@@ -224,7 +242,7 @@ function apply(ctx, config) {
224
242
  degraded = true
225
243
  lines.push(`🎬 视频约 ${(size / MB).toFixed(1)}MB,超过 ${config.video.sizeLimitMb}MB 上限,改为发送链接:`)
226
244
  lines.push(`🔗 ${link}`)
227
- if (config.video.coverOnFallback && post.coverUrl) media.push(h.image(post.coverUrl))
245
+ if (config.video.coverOnFallback && post.coverUrl) media.push(await coverEl(post.coverUrl, localize))
228
246
  kind = 'link'
229
247
  } else {
230
248
  kind = config.video.mode === 'file' ? 'file' : 'video'
@@ -237,7 +255,7 @@ function apply(ctx, config) {
237
255
  degraded = true
238
256
  lines.push(`🎬 视频下载失败(${e.message}),改为发送链接:`)
239
257
  lines.push(`🔗 ${link}`)
240
- if (config.video.coverOnFallback && post.coverUrl) media.push(h.image(post.coverUrl))
258
+ if (config.video.coverOnFallback && post.coverUrl) media.push(await coverEl(post.coverUrl, localize))
241
259
  kind = 'link'
242
260
  }
243
261
  }
@@ -256,7 +274,7 @@ function apply(ctx, config) {
256
274
  // 媒体发不出去时的兜底:文本 + 链接 + 封面
257
275
  fallback: {
258
276
  text: [text, `🔗 ${link}`].filter(Boolean).join('\n'),
259
- media: post.coverUrl ? [h.image(post.coverUrl)] : [],
277
+ media: post.coverUrl ? [await coverEl(post.coverUrl, localize)] : [],
260
278
  },
261
279
  }
262
280
  }
@@ -522,6 +540,38 @@ function apply(ctx, config) {
522
540
  return lines.join('\n')
523
541
  })
524
542
 
543
+ // -------------------------------------------------------------------------
544
+ // 见过的群
545
+ //
546
+ // 官方 QQ 没有「列出机器人所在的群」这个接口:适配器的 getGuildList() 走的是
547
+ // 频道 API,机器人只在群里时它永远是空的。群 openid 只能从群消息事件里拿,
548
+ // 所以这里把收到过消息的会话记下来,后台的群列表才有东西可选。
549
+ // -------------------------------------------------------------------------
550
+
551
+ let lastGroupSave = 0
552
+ ctx.middleware((session, next) => {
553
+ try {
554
+ const channelId = session.channelId || session.userId
555
+ if (channelId && session.platform) {
556
+ store.touchGroup({
557
+ umo: `${session.platform}:${channelId}`,
558
+ platform: session.platform,
559
+ selfId: session.selfId,
560
+ channelId,
561
+ name: (session.guild && session.guild.name) || (session.channel && session.channel.name) || '',
562
+ })
563
+ // 每条消息都落盘太重,最多一分钟写一次
564
+ if (Date.now() - lastGroupSave > 60000) {
565
+ lastGroupSave = Date.now()
566
+ store.save()
567
+ }
568
+ }
569
+ } catch (e) {
570
+ // 记录群失败不能影响正常消息
571
+ }
572
+ return next()
573
+ })
574
+
525
575
  // -------------------------------------------------------------------------
526
576
  // 控制台页面 + 接口
527
577
  // -------------------------------------------------------------------------
@@ -554,20 +604,52 @@ function apply(ctx, config) {
554
604
  ctx.console.addListener('ll-douyin/state', () => snapshot())
555
605
 
556
606
  ctx.console.addListener('ll-douyin/bots', () =>
557
- ctx.bots.map((b) => ({
558
- key: `${b.platform}|${b.selfId}`,
559
- label: `${b.platform} · ${b.selfId}${b.user && b.user.name ? ' · ' + b.user.name : ''}`,
560
- platform: b.platform,
561
- selfId: b.selfId,
562
- }))
607
+ ctx.bots
608
+ .map((b) => ({
609
+ key: `${b.platform}|${b.selfId}`,
610
+ label: `${b.platform} · ${b.selfId}${b.user && b.user.name ? ' · ' + b.user.name : ''}`,
611
+ platform: b.platform,
612
+ selfId: b.selfId,
613
+ }))
614
+ // 同一个号通常有两个适配器:qq(群聊/单聊)和 qqguild(频道)。
615
+ // 群推送要用前者,排到前面,默认就选它。
616
+ .sort((a, b) => (a.platform === 'qq' ? -1 : b.platform === 'qq' ? 1 : 0))
563
617
  )
564
618
 
565
619
  ctx.console.addListener('ll-douyin/groups', async (platform, selfId) => {
620
+ const out = new Map()
621
+ const add = (umo, name) => {
622
+ if (umo && !out.has(umo)) out.set(umo, { umo, name: name || umo })
623
+ }
624
+
625
+ // 1) 适配器能列的(只有频道场景才有内容)
566
626
  const bot = ctx.bots.find((b) => b.platform === platform && String(b.selfId) === String(selfId))
567
- if (!bot || typeof bot.getGuildList !== 'function') return []
568
- const res = await bot.getGuildList()
569
- const list = Array.isArray(res) ? res : (res && res.data) || []
570
- return list.map((g) => ({ umo: `${bot.platform}:${g.id}`, name: g.name || String(g.id) }))
627
+ if (bot && typeof bot.getGuildList === 'function') {
628
+ try {
629
+ const res = await bot.getGuildList()
630
+ const list = Array.isArray(res) ? res : (res && res.data) || []
631
+ for (const g of list) add(`${bot.platform}:${g.id}`, g.name || String(g.id))
632
+ } catch (e) {
633
+ // 群场景没有频道,正常
634
+ }
635
+ }
636
+
637
+ // 2) 数据库里见过的频道/群
638
+ if (ctx.database) {
639
+ try {
640
+ const rows = await ctx.database.get('channel', { platform })
641
+ for (const row of rows) add(`${row.platform}:${row.id}`, row.name || '')
642
+ } catch (e) {
643
+ // 没装数据库就算了
644
+ }
645
+ }
646
+
647
+ // 3) 本插件自己记录的(官方 QQ 群只能靠这个)
648
+ for (const g of store.groupsOf(platform, selfId)) {
649
+ add(g.umo, g.name || `群 ${String(g.channelId).slice(0, 6)}…`)
650
+ }
651
+
652
+ return Array.from(out.values())
571
653
  })
572
654
 
573
655
  ctx.console.addListener('ll-douyin/search', async (keyword) => {
package/lib/store.js CHANGED
@@ -17,7 +17,7 @@ const path = require('path')
17
17
  const SCHEMA_VERSION = 1
18
18
 
19
19
  function emptyData() {
20
- return { version: SCHEMA_VERSION, accounts: {}, sessions: {} }
20
+ return { version: SCHEMA_VERSION, accounts: {}, sessions: {}, groups: {} }
21
21
  }
22
22
 
23
23
  class Store {
@@ -40,6 +40,7 @@ class Store {
40
40
  version: raw.version || SCHEMA_VERSION,
41
41
  accounts: raw.accounts || {},
42
42
  sessions: raw.sessions || {},
43
+ groups: raw.groups || {},
43
44
  }
44
45
  } catch (e) {
45
46
  const backup = this.file.replace(/\.json$/, '') + `.bad.${Math.floor(Date.now() / 1000)}.json`
@@ -58,6 +59,7 @@ class Store {
58
59
  s.subs = s.subs || []
59
60
  s.latestSynced = s.latestSynced || []
60
61
  }
62
+ this.data.groups = this.data.groups || {}
61
63
  this._loaded = true
62
64
  }
63
65
 
@@ -235,6 +237,33 @@ class Store {
235
237
  return Object.values(this.data.sessions).filter((s) => s.subs.length).length
236
238
  }
237
239
 
240
+ // -- 见过的群(官方 QQ 没有群列表接口,只能靠消息事件积累) --------------
241
+
242
+ touchGroup(info) {
243
+ const g = this.data.groups || (this.data.groups = {})
244
+ const prev = g[info.umo] || {}
245
+ g[info.umo] = {
246
+ umo: info.umo,
247
+ platform: info.platform,
248
+ selfId: info.selfId || prev.selfId || '',
249
+ channelId: info.channelId,
250
+ name: info.name || prev.name || '',
251
+ lastSeen: Date.now(),
252
+ }
253
+ const keys = Object.keys(g)
254
+ if (keys.length > 300) {
255
+ keys.sort((a, b) => (g[a].lastSeen || 0) - (g[b].lastSeen || 0))
256
+ for (const k of keys.slice(0, keys.length - 300)) delete g[k]
257
+ }
258
+ }
259
+
260
+ groupsOf(platform, selfId) {
261
+ const g = this.data.groups || {}
262
+ return Object.values(g)
263
+ .filter((x) => (!platform || x.platform === platform) && (!selfId || String(x.selfId) === String(selfId)))
264
+ .sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0))
265
+ }
266
+
238
267
  dump() {
239
268
  return this.data
240
269
  }
package/package.json CHANGED
@@ -1,36 +1,36 @@
1
- {
2
- "name": "koishi-plugin-ll-douyin",
3
- "version": "0.1.0",
4
- "description": "Koishi plugin: 抖音订阅推送 —— 订阅抖音账号,新作品自动推送(图文发图、视频发视频),带控制台侧边栏管理页。",
5
- "main": "lib/index.js",
6
- "files": [
7
- "lib",
8
- "dist",
9
- "readme.md"
10
- ],
11
- "scripts": {
12
- "build": "node build.js"
13
- },
14
- "keywords": [
15
- "koishi",
16
- "plugin",
17
- "douyin",
18
- "subscribe",
19
- "qq",
20
- "push"
21
- ],
22
- "license": "MIT",
23
- "peerDependencies": {
24
- "koishi": "^4.18.0",
25
- "@koishijs/plugin-console": "^5.11.0"
26
- },
27
- "devDependencies": {
28
- "@koishijs/client": "^5.30.0"
29
- },
30
- "koishi": {
31
- "description": {
32
- "zh": "抖音订阅推送:轮询订阅账号的作品列表,图文发全部图片、视频发视频;支持体积上限降级、三级兜底(直链→下载重试→链接+封面)。自带控制台侧边栏管理页(搜索账号、选群订阅、测试推送、预览)。签名纯 JS 实现,无第三方依赖。",
33
- "en": "Subscribe to Douyin accounts and push new posts: carousels as images, videos as videos, with size-limit degradation and a 3-stage fallback. Ships a console sidebar page for searching accounts, binding groups and testing pushes."
34
- }
35
- }
36
- }
1
+ {
2
+ "name": "koishi-plugin-ll-douyin",
3
+ "version": "0.1.2",
4
+ "description": "Koishi plugin: 抖音订阅推送 —— 订阅抖音账号,新作品自动推送(图文发图、视频发视频),带控制台侧边栏管理页。",
5
+ "main": "lib/index.js",
6
+ "files": [
7
+ "lib",
8
+ "dist",
9
+ "readme.md"
10
+ ],
11
+ "scripts": {
12
+ "build": "node build.js"
13
+ },
14
+ "keywords": [
15
+ "koishi",
16
+ "plugin",
17
+ "douyin",
18
+ "subscribe",
19
+ "qq",
20
+ "push"
21
+ ],
22
+ "license": "MIT",
23
+ "peerDependencies": {
24
+ "koishi": "^4.18.0",
25
+ "@koishijs/plugin-console": "^5.11.0"
26
+ },
27
+ "devDependencies": {
28
+ "@koishijs/client": "^5.30.0"
29
+ },
30
+ "koishi": {
31
+ "description": {
32
+ "zh": "抖音订阅推送:轮询订阅账号的作品列表,图文发全部图片、视频发视频;支持体积上限降级、三级兜底(直链→下载重试→链接+封面)。自带控制台侧边栏管理页(搜索账号、选群订阅、测试推送、预览)。签名纯 JS 实现,无第三方依赖。",
33
+ "en": "Subscribe to Douyin accounts and push new posts: carousels as images, videos as videos, with size-limit degradation and a 3-stage fallback. Ships a console sidebar page for searching accounts, binding groups and testing pushes."
34
+ }
35
+ }
36
+ }
package/readme.md CHANGED
@@ -52,6 +52,9 @@ npm i koishi-plugin-ll-douyin
52
52
  所以官方 QQ 下建议把 `sizeLimitMb` 调小一点(默认 100MB 的视频转 base64 峰值内存约 300MB),
53
53
  50MB 以内比较稳。
54
54
 
55
+ 不想发视频本体的话,把 `video.mode` 设成 `link`:只发**封面图 + 作品链接**,
56
+ 最省流量,也不会被视频审核卡住。
57
+
55
58
  ## 已知限制
56
59
 
57
60
  - **不是实时**:默认 5 分钟一轮 + 抖动,账号越多间隔越长。