diff --git a/yuntu/yuntuCrowdPortraitDownload/yuntuCrowdPortraitDownload.js b/yuntu/yuntuCrowdPortraitDownload/yuntuCrowdPortraitDownload.js
deleted file mode 100644
index f4c766e..0000000
--- a/yuntu/yuntuCrowdPortraitDownload/yuntuCrowdPortraitDownload.js
+++ /dev/null
@@ -1,286 +0,0 @@
-// ==UserScript==
-// @name 批量下载人群画像
-// @namespace https://bbs.tampermonkey.net.cn/
-// @version 0.3.2
-// @description try to take over the world!
-// @author wan喜
-// @match https://yuntu.oceanengine.com/yuntu_ng/analysis/audience/list?*
-// @match https://yuntu.oceanengine.com/yuntu_brand/analysis/audience/list?*
-// @icon https://www.google.com/s2/favicons?domain=oceanengine.com
-// @require https://cdn.staticfile.net/sweetalert2/11.10.0/sweetalert2.all.min.js
-// ==/UserScript==
-
-(function () {
- "use strict"
-
- var newButton = document.createElement("button")
- newButton.innerHTML = "下载画像"
- newButton.type = "button"
- newButton.style.height = "34px"
- newButton.style.lineHeight = "34px";
- newButton.style.align = "center"; //文本居中
- newButton.style.color = "white"; //按钮文字颜色
- newButton.style.background = "#1f4bd9"; //按钮底色
- newButton.style.border = "1px solid #1f4bd9"; //边框属性
- newButton.style.borderRadius = "3px"; //按钮四个角弧度
- newButton.style.marginLeft = '10px';
- newButton.style.fontSize = '12px';
- newButton.style.padding = '0 10px';
- newButton.className = "byted-btn byted-btn-size-md byted-btn-type-primary byted-btn-shape-angle byted-can-input-grouped"
- newButton.style.marginLeft = "10px"
- newButton.addEventListener("click", handleButtonClick) //监听按钮点击事件
-
- function appendDoc() {
- setTimeout(() => {
- var a = document.querySelector(".rowFlexBox-GWvhwN")
- if (a) {
- a.append(newButton)
- }
- appendDoc()
- }, 1000)
- }
- appendDoc()
-
- const appID = "1128" //抖音
- const neededFeatureNames = ['8大消费群体', '预测年龄段', '预测性别', '城市级别'] //需要的画像维度
-
- // 在脚本的顶部定义一个全局变量
- var selectedCrowds = {
- crowdIdList: [],
- crowdNameList: []
- };
-
-
-
- function getID() {
- //从url中获取aadvid
- const url = window.location.href
- const aadvid = url.split('aadvid=')[1]
-
- // 选择所有选中的复选框所在的行
- const checkedRows = document.querySelectorAll('.yuntu_analysis-Table-Body .yuntu_analysis-Table-Row .yuntu_analysis-checkbox-checked')
- console.log(checkedRows)
- let crowdNameList = []
- let crowdIdList = []
- var invalidCrowds = []
-
- checkedRows.forEach(row => {
- // 获取人群ID,人群ID位于操作按钮的 data-log-value 属性中
- const crowdId = row.closest('tr').querySelector('.operationItem-lHPsZh[data-log-value]').getAttribute('data-log-value')
-
- // 获取人群名称,人群名称位于带有text-D50wBP类的div中
- const crowdName = row.closest('tr').querySelector('.text-D50wBP').textContent.trim()
-
- //获取人群包画像操作按钮
- const crowdProfileOperate = row.closest('tr').querySelector('.operationItem-lHPsZh[data-log-label]')
-
- //获取人群包画像状态
- const crowdProfileStatus = crowdProfileOperate.textContent.trim()
-
- // 检查画像状态是否满足条件
- if (crowdProfileStatus === "查看画像" && !crowdProfileOperate.classList.contains('disabled-I30WTJ')) { //如果是计算中的人群包,class中会有disabled-I30WTJ
- crowdIdList.push(crowdId)
- crowdNameList.push(crowdName)
- } else {
- console.log(crowdProfileStatus)
-
- invalidCrowds.push(crowdName)
- }
- })
-
- let crowdList = { crowdIdList, crowdNameList }
- console.log('crowdList:' + JSON.stringify(crowdList))
- console.log('invalidCrowds:' + invalidCrowds.join(','))
-
- return {
- aadvid,
- crowdList
- }
- }
-
- async function fetchData(url, method, body = null) {
- const headers = {
- "content-type": "application/json"
- }
-
- const options = {
- method: method,
- headers: headers
- }
-
- if (body) {
- options.body = JSON.stringify(body)
- }
-
- const response = await fetch(url, options)
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`)
- } else {
- const json = await response.json()
- return json
- //只返回data以下的部分
- }
- }
-
-
-
-
- async function fetchProfile(aadvid, crowdId, crowdName) {
- const url = "https://yuntu.oceanengine.com/yuntu_ng/api/v1/LabelDistribution?aadvid=" + aadvid + "&audience_id=" + crowdId + "&app=" + appID + "&page=1&page_size=100000&features=&label_all_cnt_filter="
- const profileJson = await fetchData(url, "GET")
- const profileArr = profileJson.data.labels
- const processedArr = profileArr
- .filter(item => neededFeatureNames.includes(item.feature_name))
- .sort((a, b) => neededFeatureNames.indexOf(a.feature_name) - neededFeatureNames.indexOf(b.feature_name))
- .map(item => {
- let result = {
- feature_name: item.feature_name,
- label_name: item.label_name
- }
- result[crowdName] = String(item.rate) // todo: crowdId 改成crowdName
- return result
- })
-
- return processedArr
- }
-
- function mergeArr(arr) {
- let mergedArray = []
- let map = new Map()
-
- arr.forEach(subArr => {
- subArr.forEach(item => {
- let key = item['feature_name'] + '-' + item['label_name']
- let existingItem = map.get(key)
- if (existingItem) {
- existingItem = { ...existingItem, ...item }
- } else {
- existingItem = { ...item }
- }
- map.set(key, existingItem)
- })
- })
-
- map.forEach(value => {
- mergedArray.push(value)
- })
-
- return mergedArray
- }
-
- class chainFunction {
- async msg() {
- const Toast = Swal.mixin({
- toast: true,
- position: "top-end",
- showConfirmButton: false,
- timer: 3000,
- })
- Toast.fire({
- icon: "success",
- title: "任务开始"
- })
- }
- async getProfile() {
- let profileArr = []
- for (let i = 0; i < this.crowdIdList.length; i++) {
- let neededProfile = await fetchProfile(this.aadvid, this.crowdIdList[i], this.crowdNameList[i])
- console.log(neededProfile) //所需要的画像维度
- profileArr.push(neededProfile)
- }
- console.log(profileArr)
- this.profileArr = profileArr
- return this
- }
- async getMergedProfile() {
- let mergedProfile = mergeArr(this.profileArr)
- this.mergedProfile = mergedProfile
- console.log(mergedProfile) //合并后的所有画像
- return this
- }
-
- async downloadCsv() {
- let csv = '';
- let keys = ['feature_name', 'label_name', ...Object.keys(this.mergedProfile[0]).filter(key => key !== 'feature_name' && key !== 'label_name')]
- csv += keys.join(',') + '\n'
- for (let i = 0; i < this.mergedProfile.length; i++) {
- let values = keys.map(key => this.mergedProfile[i][key])
- csv += values.join(',') + '\n'
- }
- var hiddenElement = document.createElement('a')
- var fileName = this.crowdNameList.map(name => name.length > 5 ? name.slice(0, 5) : name).join('+')
- hiddenElement.href = 'data:text/csv;charset=utf-8,%EF%BB%BF' + encodeURI(csv)
- hiddenElement.target = '_blank'
- hiddenElement.download = fileName.substring(0, 30) + ".csv" //只保留30个字符 防止太长Excel打不开
- hiddenElement.click()
- }
- }
-
-
-
- // 使用链式函数
- async function handleButtonClick() {
- try {
- let ID = getID()
- // 对当前页面选中的人群包进行去重处理
- ID.crowdList.crowdIdList.forEach((crowdId, index) => {
- if (!selectedCrowds.crowdIdList.includes(crowdId)) {
- selectedCrowds.crowdIdList.push(crowdId)
- selectedCrowds.crowdNameList.push(ID.crowdList.crowdNameList[index])
- }
- })
- //处理不可用的人群包
- /*
- if (ID.invalidCrowds.length > 0) {
- const Toast = Swal.mixin({
- toast: true,
- position: "top-end",
- showConfirmButton: false,
- timer: 3000,
- })
- Toast.fire({
- title: "已自动剔除没有画像的人群包: " + ID.invalidCrowds.join(', '),
- icon: "warning"
- })
- ID.invalidCrowds = []
- }
- */
- // 弹出 swal 对话框
- const { value } = await Swal.fire({
- title: '选择操作',
- html: selectedCrowds.crowdNameList.join('
'),
- icon: 'question',
- showCancelButton: true,
- confirmButtonText: '下载当前的人群',
- cancelButtonText: '继续添加'
-
- });
-
-
-
- if (value) {
- // 如果用户选择直接下载
- let chain = new chainFunction();
- await chain.msg();
- chain.aadvid = ID.aadvid;
- chain.crowdIdList = selectedCrowds.crowdIdList;
- chain.crowdNameList = selectedCrowds.crowdNameList;
- await chain.getProfile();
- await chain.getMergedProfile();
- await chain.downloadCsv();
- // 下载后清空全局变量
- selectedCrowds.crowdIdList = [];
- selectedCrowds.crowdNameList = [];
- } else {
- // 如果用户选择继续添加人群包,什么也不做,允许用户切换页面继续选择
- }
- } catch (error) {
- console.error(error);
- }
- }
-
-
-
-
-})()
\ No newline at end of file
diff --git a/yuntu/yuntuCrowdPortraitDownload/yuntuCrowdPortraitDownload.user.js b/yuntu/yuntuCrowdPortraitDownload/yuntuCrowdPortraitDownload.user.js
new file mode 100644
index 0000000..7160f8d
--- /dev/null
+++ b/yuntu/yuntuCrowdPortraitDownload/yuntuCrowdPortraitDownload.user.js
@@ -0,0 +1,803 @@
+// ==UserScript==
+// @name 批量下载人群画像
+// @namespace https://bbs.tampermonkey.net.cn/
+// @version 1.0.0
+// @description 批量下载巨量云图人群画像
+// @author 汪喜
+// @match https://yuntu.oceanengine.com/yuntu_ng/analysis/audience/list?*
+// @match https://yuntu.oceanengine.com/yuntu_brand/analysis/audience/list?*
+// @match https://yuntu.oceanengine.com/yuntu_brand/ecom/analysis/audience/list?*
+// @icon https://www.google.com/s2/favicons?domain=oceanengine.com
+// @require https://cdn.staticfile.net/sweetalert2/11.10.0/sweetalert2.all.min.js
+// ==/UserScript==
+
+(function () {
+ "use strict"
+
+ var newButton = document.createElement("button")
+ newButton.innerHTML = "下载画像"
+ newButton.type = "button"
+ newButton.style.height = "34px"
+ newButton.style.lineHeight = "34px";
+ newButton.style.align = "center"; //文本居中
+ newButton.style.color = "white"; //按钮文字颜色
+ newButton.style.background = "#a52615"; //按钮底色
+ newButton.style.border = "1px solid #a52615"; //边框属性
+ newButton.style.borderRadius = "3px"; //按钮四个角弧度
+ newButton.style.marginLeft = '10px';
+ newButton.style.fontSize = '12px';
+ newButton.style.padding = '0 10px';
+ newButton.className = "byted-btn byted-btn-size-md byted-btn-type-primary byted-btn-shape-angle byted-can-input-grouped"
+ newButton.style.marginLeft = "10px"
+ newButton.addEventListener("click", handleButtonClick) //监听按钮点击事件
+
+ function appendDoc() {
+ setTimeout(() => {
+ const toolbar = document.querySelector(".rowFlexBox-oqJGZk, .rowFlexBox-GWvhwN")
+ if (toolbar && newButton.parentElement !== toolbar) {
+ toolbar.append(newButton)
+ }
+ appendDoc()
+ }, 1000)
+ }
+ appendDoc()
+
+ const portraitFeatureOptions = [
+ {
+ key: 'yuntu_city_level',
+ label: '城市级别',
+ requestKeys: ['yuntu_city_level'],
+ responseKeys: ['yuntu_city_level'],
+ responseZh: ['城市级别']
+ },
+ {
+ key: 'gender',
+ label: '预测性别',
+ requestKeys: ['gender'],
+ responseKeys: ['gender'],
+ responseZh: ['预测性别']
+ },
+ {
+ key: 'new_age_v2',
+ label: '预测年龄段',
+ requestKeys: ['new_age_v2'],
+ responseKeys: ['new_age_v2'],
+ responseZh: ['预测年龄段']
+ },
+ {
+ key: 'ecom_big8_audience',
+ label: '八大消费群体',
+ requestKeys: ['ecom_big8_audience'],
+ responseKeys: ['ecom_big8_audience'],
+ responseZh: ['八大消费群体', '8大消费群体']
+ },
+ {
+ key: 'consuming_capacity',
+ label: '预测消费能力',
+ requestKeys: ['consuming_capacity'],
+ responseKeys: ['consuming_capacity'],
+ responseZh: ['预测消费能力']
+ },
+ {
+ key: 'life_stage',
+ label: '预测人生阶段',
+ requestKeys: ['life_stage'],
+ responseKeys: ['life_stage'],
+ responseZh: ['预测人生阶段']
+ },
+ {
+ key: 'city',
+ label: '城市',
+ requestKeys: ['city'],
+ responseKeys: ['city'],
+ responseZh: ['城市']
+ },
+ {
+ key: 'province',
+ label: '省份',
+ requestKeys: ['province'],
+ responseKeys: ['province'],
+ responseZh: ['省份']
+ },
+ {
+ key: 'phone_price_preference',
+ label: '手机价格',
+ requestKeys: ['phone_price_preference'],
+ responseKeys: ['phone_price_preference'],
+ responseZh: ['手机价格']
+ },
+ {
+ key: 'douyin_active_user',
+ label: '抖音活跃用户',
+ requestKeys: ['douyin_active_user'],
+ responseKeys: ['douyin_active_user'],
+ responseZh: ['抖音活跃用户']
+ }
+ ]
+ const defaultSelectedFeatureKeys = [
+ 'yuntu_city_level',
+ 'gender',
+ 'new_age_v2',
+ 'ecom_big8_audience',
+ 'life_stage',
+ 'consuming_capacity'
+ ]
+ const industryVersion = "10005"
+ const pathId = "analysis_audience_portrait"
+ const runtimeContext = {
+ brandContext: null
+ }
+
+ // 在脚本的顶部定义一个全局变量
+ var selectedCrowds = {
+ crowdIdList: [],
+ crowdNameList: [],
+ coverNumberList: []
+ };
+
+
+
+ function getCellText(row, colIndex, selector = null) {
+ const cell = row.querySelector(`td[aria-colindex="${colIndex}"]`)
+ if (!cell) {
+ return ""
+ }
+
+ const target = selector ? cell.querySelector(selector) : cell
+ return target ? target.textContent.trim() : ""
+ }
+
+ function getCrowdProfileOperate(row) {
+ const operationItems = Array.from(row.querySelectorAll('[data-log-label][data-log-value]'))
+ return operationItems.find(item => {
+ const label = item.getAttribute('data-log-label') || ""
+ const text = item.textContent.trim()
+ return label.includes('画像') || text.includes('画像')
+ }) || null
+ }
+
+ function normalizeCoverNumber(text) {
+ const normalized = String(text || "").replace(/,/g, "").trim()
+ return normalized && normalized !== "-" ? normalized : ""
+ }
+
+ function getID() {
+ const url = new URL(window.location.href)
+ const aadvid = url.searchParams.get('aadvid') || ""
+
+ const checkedRows = document.querySelectorAll('.yuntu_analysis-Table-Body .yuntu_analysis-checkbox-checked')
+ console.log(checkedRows)
+ let crowdNameList = []
+ let crowdIdList = []
+ let coverNumberList = []
+ var invalidCrowds = []
+
+ checkedRows.forEach(checkbox => {
+ const row = checkbox.closest('tr')
+ if (!row) {
+ return
+ }
+
+ const crowdName = getCellText(row, 2, '.text-jIwxn6') || getCellText(row, 2)
+ const crowdId = getCellText(row, 3) || (getCrowdProfileOperate(row)?.getAttribute('data-log-value') || "")
+ const coverNumber = normalizeCoverNumber(getCellText(row, 4))
+ const crowdProfileOperate = getCrowdProfileOperate(row)
+ const crowdProfileStatus = crowdProfileOperate ? crowdProfileOperate.textContent.trim() : ""
+ const crowdStatusText = getCellText(row, 5, '.txt-KZswxE') || getCellText(row, 5)
+ const isDisabled = crowdProfileOperate ? /disabled/.test(crowdProfileOperate.className) : true
+
+ if (!crowdId || !crowdName || !crowdProfileOperate) {
+ invalidCrowds.push(crowdName || crowdId || '未知人群')
+ return
+ }
+
+ if (crowdProfileStatus === "查看画像" && !isDisabled && crowdStatusText === "计算完成") {
+ crowdIdList.push(crowdId)
+ crowdNameList.push(crowdName)
+ coverNumberList.push(coverNumber)
+ } else {
+ console.log(`${crowdName} 状态不满足下载条件: ${crowdStatusText} / ${crowdProfileStatus}`)
+ invalidCrowds.push(crowdName)
+ }
+ })
+
+ let crowdList = { crowdIdList, crowdNameList, coverNumberList }
+ console.log('crowdList:' + JSON.stringify(crowdList))
+ console.log('invalidCrowds:' + invalidCrowds.join(','))
+
+ return {
+ aadvid,
+ crowdList,
+ invalidCrowds
+ }
+ }
+
+ function normalizeBrandMetadata(list) {
+ const candidates = []
+ const seen = new Set()
+
+ for (const item of Array.isArray(list) ? list : []) {
+ const industryId = String(item?.industry_id || "")
+ if (industryId.length !== 2) {
+ continue
+ }
+
+ const brandId = String(item?.brand_id || "")
+ const brandName = String(item?.brand_name || "")
+ const industryName = String(item?.industry_name || "")
+ const key = `${brandId}|${industryId}`
+
+ if (!brandId || !industryId || seen.has(key)) {
+ continue
+ }
+
+ seen.add(key)
+ candidates.push({
+ key,
+ brandId,
+ brandName,
+ industryId,
+ industryName
+ })
+ }
+
+ return candidates
+ }
+
+ async function requestJson(url, { method = "GET", body = null, headers = {}, referrer = window.location.href } = {}) {
+ const response = await fetch(url, {
+ method,
+ headers,
+ body: body ? JSON.stringify(body) : null,
+ credentials: "include",
+ mode: "cors",
+ referrer
+ })
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`)
+ }
+
+ return response.json()
+ }
+
+ async function resolveBrandContext(aadvid) {
+ if (runtimeContext.brandContext?.aadvid === aadvid) {
+ return runtimeContext.brandContext
+ }
+
+ const response = await requestJson(
+ `https://yuntu.oceanengine.com/yuntu_ng/api/v1/get_user_info?aadvid=${encodeURIComponent(aadvid)}`,
+ {
+ method: "GET",
+ referrer: window.location.href
+ }
+ )
+ const candidates = normalizeBrandMetadata(response?.data?.brandMetadata)
+
+ if (candidates.length === 0) {
+ throw new Error("未从 get_user_info 中解析到品牌和行业信息")
+ }
+
+ const selected = candidates[0]
+
+ runtimeContext.brandContext = {
+ aadvid,
+ brandId: selected.brandId,
+ brandName: selected.brandName,
+ industryId: selected.industryId,
+ industryName: selected.industryName,
+ industryVersion
+ }
+
+ return runtimeContext.brandContext
+ }
+
+ function getPortraitReferrer(crowdId, aadvid) {
+ const url = new URL(window.location.href)
+ url.pathname = url.pathname.replace(/\/list$/, "/portrait_v2")
+ url.search = ""
+ url.searchParams.set("cid", crowdId)
+ url.searchParams.set("aadvid", aadvid)
+ return url.toString()
+ }
+
+ function getPortraitHeaders() {
+ return {
+ "content-type": "application/json"
+ }
+ }
+
+ async function fetchAudienceBcId(aadvid, crowdId, brandContext) {
+ const requestUrl = new URL("https://yuntu.oceanengine.com/yuntu_ng/api/v1/GetAudienceBcId")
+ requestUrl.searchParams.set("aadvid", aadvid)
+ requestUrl.searchParams.set("custom_audience_id", crowdId)
+ requestUrl.searchParams.set("brand_id", brandContext.brandId)
+ requestUrl.searchParams.set("level_1_industy_id", brandContext.industryId)
+
+ const response = await requestJson(requestUrl.toString(), {
+ method: "GET",
+ referrer: getPortraitReferrer(crowdId, aadvid)
+ })
+
+ console.log("GetAudienceBcId response:", response)
+
+ const bcId = String(response?.data?.bcId || "")
+
+ if (!bcId) {
+ throw new Error(`未获取到人群 ${crowdId} 的 bcID`)
+ }
+
+ return bcId
+ }
+
+ function getFeatureOptionMap() {
+ return portraitFeatureOptions.reduce((acc, item) => {
+ acc[item.key] = item
+ return acc
+ }, {})
+ }
+
+ function getSelectedFeatureOptions(selectedFeatureKeys) {
+ const featureKeySet = new Set(selectedFeatureKeys)
+ return portraitFeatureOptions.filter(item => featureKeySet.has(item.key))
+ }
+
+ function buildFeatureRequestKeys(selectedFeatureKeys) {
+ const requestKeys = []
+
+ getSelectedFeatureOptions(selectedFeatureKeys).forEach(item => {
+ item.requestKeys.forEach(key => {
+ if (!requestKeys.includes(key)) {
+ requestKeys.push(key)
+ }
+ })
+ })
+
+ return requestKeys
+ }
+
+ function renderFeatureSelectorHtml(selectedFeatureKeys) {
+ const featureKeySet = new Set(selectedFeatureKeys)
+ const checkboxHtml = portraitFeatureOptions.map(item => {
+ const checked = featureKeySet.has(item.key) ? 'checked' : ''
+ return `
+
+ `
+ }).join('')
+
+ return `
+