diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..c1b0b47
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,15 @@
+**/node_modules
+**/.git
+logs
+videos
+db
+bin
+obj
+.claude
+.playwright-mcp
+.idea
+.codepilot-uploads
+*.tar.gz
+.DS_Store
+**/.DS_Store
+smoke-*.png
diff --git a/Controllers/ConfigController.cs b/Controllers/ConfigController.cs
index 0d8ec41..ce71c9c 100644
--- a/Controllers/ConfigController.cs
+++ b/Controllers/ConfigController.cs
@@ -25,11 +25,8 @@ namespace dy.net.Controllers
private readonly DouyinFollowService douyinFollowService;
private readonly DouyinCookieService douyinCookieService;
private readonly DouyinHttpClientService httpClientService;
- private readonly DyQrCodeService _qrCodeService;
-
-
- public ConfigController(DouyinCookieService dyCookieService, DouyinCommonService commonService, DouyinQuartzJobService quartzJobService, DouyinFollowService douyinFollowService, DouyinCookieService douyinCookieService, DouyinHttpClientService httpClientService, DyQrCodeService qrCodeService)
+ public ConfigController(DouyinCookieService dyCookieService, DouyinCommonService commonService, DouyinQuartzJobService quartzJobService, DouyinFollowService douyinFollowService, DouyinCookieService douyinCookieService, DouyinHttpClientService httpClientService)
{
this.dyCookieService = dyCookieService;
this.commonService = commonService;
@@ -37,7 +34,6 @@ namespace dy.net.Controllers
this.douyinFollowService = douyinFollowService;
this.douyinCookieService = douyinCookieService;
this.httpClientService = httpClientService;
- _qrCodeService = qrCodeService;
}
@@ -487,64 +483,5 @@ namespace dy.net.Controllers
}
}
- ///
- /// 获取抖音扫码登录二维码
- ///
- [AllowAnonymous]
- [HttpGet("qrcode")]
- public async Task GetQrCode()
- {
- try
- {
- var (token, qrBase64) = await _qrCodeService.GetQrCodeAsync();
- return Ok(new { code = 0, data = new { token, qrBase64 } });
- }
- catch (Exception ex)
- {
- return Ok(new { code = -1, error = ex.Message });
- }
- }
-
- ///
- /// 轮询扫码状态(前端每2秒调一次)
- /// status: 1=待扫码, 2=已扫码待确认, 3=登录成功, -1=已过期
- ///
- [AllowAnonymous]
- [HttpGet("qrstatus")]
- public async Task GetQrStatus([FromQuery] string token)
- {
- try
- {
- var (status, cookies, secUserId) = await _qrCodeService.CheckStatusAsync(token);
- return Ok(new { code = 0, data = new { status, cookies, secUserId } });
- }
- catch (Exception ex)
- {
- Serilog.Log.Error($"查询扫码状态失败: {ex.Message}");
- return Ok(new { code = -1, error = ex.Message });
- }
- }
-
- ///
- /// 扫码成功后,将 Cookie 绑定到指定账号
- ///
- [HttpPost("qrbind")]
- public async Task BindQrCode([FromQuery] string token, [FromQuery] string id)
- {
- try
- {
- var (cookies, secUserId) = _qrCodeService.GetSessionResult(token);
- if (string.IsNullOrEmpty(cookies))
- return Ok(new { code = -1, error = "扫码尚未完成或已过期" });
-
- var result = await douyinCookieService.FastResetCookie(id, cookies);
- _qrCodeService.RemoveSession(token);
- return Ok(new { code = result ? 0 : -1, secUserId });
- }
- catch (Exception ex)
- {
- return Ok(new { code = -1, error = ex.Message });
- }
- }
}
}
diff --git a/Dockerfile.prod b/Dockerfile.prod
index 6f955b9..d4a7e08 100644
--- a/Dockerfile.prod
+++ b/Dockerfile.prod
@@ -10,7 +10,7 @@ RUN npm config set registry https://registry.npmmirror.com && \
npm install -g pnpm && \
pnpm config set registry https://registry.npmmirror.com && \
if [ -f pnpm-lock.yaml ]; then \
- pnpm install --frozen-lockfile; \
+ pnpm install --no-frozen-lockfile; \
else \
npm install; \
fi
@@ -18,17 +18,11 @@ RUN npm config set registry https://registry.npmmirror.com && \
COPY app/ ./
RUN pnpm build || npm run build
-# 第二阶段:构建后端 + 下载 Playwright Chromium
-FROM mcr.azurecr.cn/dotnet/sdk:6.0 AS backend-builder
+# 第二阶段:构建后端
+FROM mcr.microsoft.com/dotnet/sdk:6.0 AS backend-builder
WORKDIR /src
-# 安装 Node.js 18(用于 npx playwright install,最可靠的方式)
-RUN apt-get update && apt-get install -y curl && \
- curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
- apt-get install -y nodejs && \
- rm -rf /var/lib/apt/lists/*
-
COPY *.csproj ./
RUN dotnet restore dy.net.csproj
@@ -39,46 +33,18 @@ COPY --from=frontend-builder /app/frontend/dist ./app/dist
RUN dotnet publish dy.net.csproj -c Release -o /app/publish
-# 用 npx 下载 Playwright Chromium 到固定路径
-ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
-RUN npx -y playwright@1.44.0 install chromium
-
-# 第三阶段:运行镜像
-FROM mcr.azurecr.cn/dotnet/aspnet:6.0 AS final
+# 第三阶段:运行镜像(Alpine 版:apk 包更小、代理下更易通过)
+FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine AS final
WORKDIR /app
ENV TZ=Asia/Shanghai
-RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
-
-# 切换阿里云镜像源,解决国内网络无法访问 deb.debian.org 的问题
-RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \
- sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list
-
-# 安装 Chromium 运行时依赖(Debian Bullseye)
-RUN apt-get update && apt-get install -y --no-install-recommends \
- libnss3 \
- libatk1.0-0 \
- libatk-bridge2.0-0 \
- libcups2 \
- libdrm2 \
- libxkbcommon0 \
- libxcomposite1 \
- libxdamage1 \
- libxfixes3 \
- libxrandr2 \
- libgbm1 \
- libasound2 \
- libpango-1.0-0 \
- libpangocairo-1.0-0 \
- libgtk-3-0 \
- fonts-liberation \
- && rm -rf /var/lib/apt/lists/*
+RUN apk add --no-cache tzdata ffmpeg && \
+ cp /usr/share/zoneinfo/$TZ /etc/localtime && \
+ echo $TZ > /etc/timezone
COPY --from=backend-builder /app/publish .
-COPY --from=backend-builder /ms-playwright /ms-playwright
-ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
EXPOSE 10101
ENV ASPNETCORE_URLS=http://*:10101
ENV ASPNETCORE_ENVIRONMENT=Production
diff --git a/app/src/App.vue b/app/src/App.vue
index b2d021c..200fbf1 100644
--- a/app/src/App.vue
+++ b/app/src/App.vue
@@ -132,6 +132,83 @@ body {
height: 100vh;
overflow-y: hidden;
}
+
+// 小屏下 Modal / Drawer 全局滚动策略
+// 挂载到 document.body 的元素 scoped 样式无法命中,此处非 scoped 全局生效
+// 策略:让 wrap 承担滚动,不强制 body 内滚——这样鼠标在 modal 任意位置滚轮都能滚,避免 Mac overlay scrollbar 手感"滑不动"
+.ant-modal-wrap {
+ overflow-y: auto !important; // wrap 是 fixed 定位滚动容器;antd 默认就是 auto,此处 !important 兜底
+ -webkit-overflow-scrolling: touch; // 移动端惯性滚动
+
+ // 强制显示滚动条(Mac overlay 默认隐藏,用户误以为"滑不动")
+ &::-webkit-scrollbar {
+ width: 10px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: rgba(0, 0, 0, 0.35);
+ border-radius: 5px;
+ &:hover { background: rgba(0, 0, 0, 0.55); }
+ }
+ &::-webkit-scrollbar-track {
+ background: rgba(255, 255, 255, 0.1);
+ }
+}
+
+.ant-modal {
+ top: 20px !important; // 覆盖 antd 通过 inline style 设的 top: 100px
+ padding-bottom: 20px !important;
+ margin-bottom: 20px;
+}
+
+// body 承担滚动:max-height 硬约束,内容超出时滚,内容少时自然收缩(modal 不会撑成一大块留白)
+// 100vh - 150px = 减去 top(20) + margin-bottom(20) + header(约 55) + padding(约 40) + 保险 15
+.ant-modal .ant-modal-body {
+ max-height: calc(100vh - 150px) !important;
+ overflow-y: auto !important;
+
+ &::-webkit-scrollbar {
+ width: 10px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: rgba(0, 0, 0, 0.35);
+ border-radius: 5px;
+ &:hover { background: rgba(0, 0, 0, 0.55); }
+ }
+ &::-webkit-scrollbar-track {
+ background: rgba(0, 0, 0, 0.05);
+ }
+}
+
+// 抖音授权 Cookie 编辑用 full-modal(顶部对齐的宽 modal)
+.full-modal .ant-modal {
+ top: 0 !important;
+ padding-bottom: 0 !important;
+ margin-bottom: 0;
+}
+// full-modal 没有 top 留白,body max-height 减去的常量更小
+.full-modal .ant-modal-body {
+ max-height: calc(100vh - 110px) !important;
+}
+
+// 登录页整个界面本身就是一个 Modal(LoginModal.vue),必须恢复 antd 默认布局,不能被上面的全局规则改
+.login-modal .ant-modal {
+ top: 100px !important;
+ padding-bottom: 24px !important;
+ margin-bottom: 0;
+}
+
+// 忘记密码引导 Modal(LoginBox.vue 里的 forgot-modal)同样保留默认布局
+.forgot-modal .ant-modal {
+ top: 100px !important;
+ padding-bottom: 24px !important;
+ margin-bottom: 0;
+}
+
+// Drawer 组件(抽屉)也保证内容可滚
+.ant-drawer-body {
+ overflow-y: auto !important;
+}
+
.stepin-img-checkbox {
@apply transition-transform;
&:hover {
diff --git a/app/src/components/video/VideoPlayer.vue b/app/src/components/video/VideoPlayer.vue
index f5f6bed..41a70fe 100644
--- a/app/src/components/video/VideoPlayer.vue
+++ b/app/src/components/video/VideoPlayer.vue
@@ -125,7 +125,7 @@ const loadVideoStream = async (videoId: string) => {
// 构建视频流 URL,浏览器会自动发送 Range 请求
// 添加时间戳避免缓存问题
- const url = `/api/video/stream/${videoId}?t=${Date.now()}`;
+ const url = `/api/Video/play/${videoId}?t=${Date.now()}`;
console.log('[VideoPlayer] 设置视频流 URL:', url);
// 直接使用 URL,让浏览器处理 Range 请求
@@ -555,11 +555,12 @@ defineExpose({
.video-element {
width: 100%;
height: auto;
- max-height: 70vh;
+ max-height: 45vh;
display: block;
}
}
+
.loading-overlay {
position: absolute;
top: 0;
@@ -578,3 +579,15 @@ defineExpose({
}
+
+
diff --git a/app/src/pages/cok/CookieTable.vue b/app/src/pages/cok/CookieTable.vue
index 6f04a75..3d588a7 100644
--- a/app/src/pages/cok/CookieTable.vue
+++ b/app/src/pages/cok/CookieTable.vue
@@ -20,17 +20,12 @@ import { message } from 'ant-design-vue';
import { StarOutlined, StarFilled, StarTwoTone } from '@ant-design/icons-vue';
const columns = ref([]);
columns.value = [
- {
- title: 'Cookie名称',
- dataIndex: 'userName',
- width: 180,
- },
- { title: 'Cookie状态', dataIndex: 'statusMsg' },
- { title: '收藏路径', dataIndex: 'savePath' },
- { title: '喜欢路径', dataIndex: 'favSavePath' },
- { title: '博主路径', dataIndex: 'upSavePath' },
- { title: '状态', dataIndex: 'status', width: 180 },
- { title: '操作', dataIndex: 'edit', width: 200 },
+ { title: 'Cookie名称', dataIndex: 'userName', width: 140 },
+ { title: '收藏路径', dataIndex: 'savePath', ellipsis: true, minWidth: 120 },
+ { title: '喜欢路径', dataIndex: 'favSavePath', ellipsis: true, minWidth: 120 },
+ { title: '博主路径', dataIndex: 'upSavePath', ellipsis: true, minWidth: 120 },
+ { title: '状态', dataIndex: 'status', width: 160 },
+ { title: '操作', dataIndex: 'edit', width: 220 },
];
interface UpSecUserIdItem {
@@ -276,7 +271,7 @@ const removeRow = (index: number) => {
form.upSecUserIdsJson.splice(index, 1);
}
};
-const rowCount = 10;
+const rowCount = 6;
onMounted(() => {
GetRecords();
@@ -472,10 +467,12 @@ const saveDrawerData = () => {
const switchdownCollect = (e: any) => {
if (!e) form.useCollectFolder = e;
};
+
+
@@ -600,6 +597,7 @@ const switchdownCollect = (e: any) => {
+
@@ -654,7 +652,7 @@ const switchdownCollect = (e: any) => {
-
+
@@ -704,9 +702,31 @@ const switchdownCollect = (e: any) => {
+
\ No newline at end of file
diff --git a/app/src/store/coreapi.ts b/app/src/store/coreapi.ts
index c6d20fb..cdab716 100644
--- a/app/src/store/coreapi.ts
+++ b/app/src/store/coreapi.ts
@@ -136,7 +136,6 @@ export const useApiStore = defineStore('coreapi', () => {
});
}
-
async function UpdateConfig(param: object) {
return http.request>('/api/config/update', 'post_json', param).then(r => {
return r;
diff --git a/app/vite.config.ts b/app/vite.config.ts
index a82a39a..6fdf1ee 100644
--- a/app/vite.config.ts
+++ b/app/vite.config.ts
@@ -34,6 +34,9 @@ export default ({ command, mode }) => {
cors: true,
port: 8001,
open: false, //自动打开
+ watch: {
+ usePolling: true, // 绕过 fsevents 架构不兼容问题(arm64 Mac)
+ },
proxy: {
'/api': {
target: env.VITE_API_URL,
diff --git a/dy.net.csproj b/dy.net.csproj
index ab4ff19..537dc8a 100644
--- a/dy.net.csproj
+++ b/dy.net.csproj
@@ -76,7 +76,6 @@
-
diff --git a/extension/ServiceExtension.cs b/extension/ServiceExtension.cs
index bdc2eb3..28dab04 100644
--- a/extension/ServiceExtension.cs
+++ b/extension/ServiceExtension.cs
@@ -309,8 +309,6 @@ namespace dy.net.extension
});
services.AddScoped();
- // 扫码登录服务(Playwright,全局单例避免重复初始化)
- services.AddSingleton();
services.AddScoped();
}
diff --git a/service/DyQrCodeService.cs b/service/DyQrCodeService.cs
deleted file mode 100644
index 57480e8..0000000
--- a/service/DyQrCodeService.cs
+++ /dev/null
@@ -1,153 +0,0 @@
-using System.Collections.Concurrent;
-using Microsoft.Playwright;
-
-namespace dy.net.service
-{
- public class DyQrCodeService
- {
- private static readonly ConcurrentDictionary _sessions = new();
-
- private class QrSession
- {
- public IPlaywright Playwright { get; set; }
- public IBrowser Browser { get; set; }
- public IBrowserContext Context { get; set; }
- public IPage Page { get; set; }
- public DateTime CreatedAt { get; set; } = DateTime.Now;
- public string FinalCookies { get; set; }
- public string SecUserId { get; set; }
- }
-
- private const string LoginUrl = "https://creator.douyin.com/creator-micro/login";
- // QR 码图片的 class 名含 qrcode_img(后缀随版本变化,用属性选择器匹配)
- private const string QrSelector = "img[class*='qrcode_img']";
-
- public async Task<(string token, string qrBase64)> GetQrCodeAsync()
- {
- CleanExpiredSessions();
-
- var playwright = await Playwright.CreateAsync();
- var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
- {
- Headless = true,
- Args = new[] { "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage" }
- });
- var context = await browser.NewContextAsync(new BrowserNewContextOptions
- {
- UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
- });
- var page = await context.NewPageAsync();
-
- await page.GotoAsync(LoginUrl, new PageGotoOptions { Timeout = 15000 });
- await page.WaitForSelectorAsync(QrSelector, new PageWaitForSelectorOptions { Timeout = 10000 });
-
- // img.src 已经是 data:image/png;base64,... 直接取用
- var src = await page.EvaluateAsync($"() => document.querySelector(\"{QrSelector}\").src");
- var qrBase64 = src.Replace("data:image/png;base64,", "");
-
- var token = Guid.NewGuid().ToString("N");
- _sessions[token] = new QrSession
- {
- Playwright = playwright,
- Browser = browser,
- Context = context,
- Page = page
- };
-
- return (token, qrBase64);
- }
-
- public async Task<(int status, string cookies, string secUserId)> CheckStatusAsync(string token)
- {
- if (!_sessions.TryGetValue(token, out var session))
- return (-1, null, null);
-
- if (DateTime.Now - session.CreatedAt > TimeSpan.FromMinutes(5))
- {
- await CloseSessionAsync(token, session);
- return (-1, null, null);
- }
-
- // 已经登录过,直接返回缓存
- if (session.FinalCookies != null)
- return (3, session.FinalCookies, session.SecUserId);
-
- var url = session.Page.Url;
-
- // 还在登录页
- if (url.Contains("/creator-micro/login") || url.Contains("sso.douyin.com"))
- {
- // 判断是否已扫码(页面出现"确认"或"已扫码"文字)
- var scanned = await session.Page.EvaluateAsync(
- "() => { const t = document.body.innerText; return t.includes('已扫码') || t.includes('确认登录'); }"
- );
- return (scanned ? 2 : 1, null, null);
- }
-
- // URL 已离开登录页,说明登录成功
- var browserCookies = await session.Context.CookiesAsync(new[]
- {
- "https://creator.douyin.com",
- "https://www.douyin.com",
- "https://sso.douyin.com"
- });
- var cookieStr = string.Join("; ", browserCookies.Select(c => $"{c.Name}={c.Value}"));
-
- // 尝试从页面获取 sec_user_id
- var secUserId = await GetSecUserIdAsync(session.Page);
-
- session.FinalCookies = cookieStr;
- session.SecUserId = secUserId;
-
- // 登录完成后关闭浏览器释放资源
- _ = Task.Run(async () => await CloseSessionAsync(token, session));
-
- return (3, cookieStr, secUserId);
- }
-
- public (string cookies, string secUserId) GetSessionResult(string token)
- {
- if (_sessions.TryGetValue(token, out var session))
- return (session.FinalCookies ?? "", session.SecUserId ?? "");
- return ("", "");
- }
-
- public void RemoveSession(string token)
- {
- if (_sessions.TryRemove(token, out var session))
- _ = Task.Run(async () => await CloseSessionAsync(token, session));
- }
-
- private static async Task GetSecUserIdAsync(IPage page)
- {
- try
- {
- // 从页面 URL 或 API 提取 sec_user_id
- var resp = await page.EvaluateAsync(
- "async () => { const r = await fetch('/web/api/userinfo/'); const j = await r.json(); return j?.data?.sec_uid || j?.sec_uid || ''; }"
- );
- return resp ?? "";
- }
- catch
- {
- return "";
- }
- }
-
- private static async Task CloseSessionAsync(string token, QrSession session)
- {
- _sessions.TryRemove(token, out _);
- try { await session.Browser?.CloseAsync(); } catch { }
- try { session.Playwright?.Dispose(); } catch { }
- }
-
- private static void CleanExpiredSessions()
- {
- var expired = _sessions
- .Where(kv => DateTime.Now - kv.Value.CreatedAt > TimeSpan.FromMinutes(6))
- .ToList();
- foreach (var kv in expired)
- _ = Task.Run(async () => await CloseSessionAsync(kv.Key, kv.Value));
- }
- }
-}